@astram/identity 0.1.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/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/branch.object.d.ts +6 -0
- package/dist/branch.object.d.ts.map +1 -0
- package/dist/branch.object.js +37 -0
- package/dist/company-branch.object.d.ts +11 -0
- package/dist/company-branch.object.d.ts.map +1 -0
- package/dist/company-branch.object.js +42 -0
- package/dist/company-department.object.d.ts +13 -0
- package/dist/company-department.object.d.ts.map +1 -0
- package/dist/company-department.object.js +44 -0
- package/dist/company-membership.object.d.ts +16 -0
- package/dist/company-membership.object.d.ts.map +1 -0
- package/dist/company-membership.object.js +63 -0
- package/dist/company.object.d.ts +10 -0
- package/dist/company.object.d.ts.map +1 -0
- package/dist/company.object.js +64 -0
- package/dist/department.object.d.ts +6 -0
- package/dist/department.object.d.ts.map +1 -0
- package/dist/department.object.js +37 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/membership-posting.object.d.ts +16 -0
- package/dist/membership-posting.object.d.ts.map +1 -0
- package/dist/membership-posting.object.js +76 -0
- package/dist/migrations-dir.d.ts +2 -0
- package/dist/migrations-dir.d.ts.map +1 -0
- package/dist/migrations-dir.js +14 -0
- package/dist/role.object.d.ts +7 -0
- package/dist/role.object.d.ts.map +1 -0
- package/dist/role.object.js +42 -0
- package/dist/schema.d.ts +3 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +5 -0
- package/dist/user.object.d.ts +13 -0
- package/dist/user.object.d.ts.map +1 -0
- package/dist/user.object.js +59 -0
- package/migrations/1710000000001-CreateIdentityOrgModel.cjs +100 -0
- package/migrations/1710000000002-CreateMembershipAndPostings.cjs +111 -0
- package/migrations/1710000000003-CreateIdentityCredential.cjs +58 -0
- package/migrations/1710000000004-CreateSessionTables.cjs +79 -0
- package/migrations/1710000000005-CreateMfaAndTokenSurface.cjs +88 -0
- package/package.json +50 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.Role = void 0;
|
|
13
|
+
const engine_1 = require("@astram/engine");
|
|
14
|
+
const schema_1 = require("./schema");
|
|
15
|
+
/** roles — global catalog. */
|
|
16
|
+
let Role = class Role {
|
|
17
|
+
uuid;
|
|
18
|
+
name;
|
|
19
|
+
createdAt;
|
|
20
|
+
};
|
|
21
|
+
exports.Role = Role;
|
|
22
|
+
__decorate([
|
|
23
|
+
(0, engine_1.Field)({ column: 'r_uuid', type: 'uuid', role: 'primaryKey' }),
|
|
24
|
+
__metadata("design:type", String)
|
|
25
|
+
], Role.prototype, "uuid", void 0);
|
|
26
|
+
__decorate([
|
|
27
|
+
(0, engine_1.Field)({ column: 'r_name', type: 'string', required: true }),
|
|
28
|
+
__metadata("design:type", String)
|
|
29
|
+
], Role.prototype, "name", void 0);
|
|
30
|
+
__decorate([
|
|
31
|
+
(0, engine_1.Field)({ column: 'r_created_at', type: 'timestamp' }),
|
|
32
|
+
__metadata("design:type", String)
|
|
33
|
+
], Role.prototype, "createdAt", void 0);
|
|
34
|
+
exports.Role = Role = __decorate([
|
|
35
|
+
(0, engine_1.ObjectType)({
|
|
36
|
+
typeKey: 'role',
|
|
37
|
+
table: 'roles',
|
|
38
|
+
schema: schema_1.SCHEMA,
|
|
39
|
+
access: { plane: 'control', system: true },
|
|
40
|
+
}),
|
|
41
|
+
(0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'r_uuid' })
|
|
42
|
+
], Role);
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,eAAO,MAAM,MAAM,eAAe,CAAC"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* users — the principal. `u_uuid` links to identity_credential.ic_principal_id
|
|
3
|
+
* (the hash lives framework-side). Users are not tenant-stamped (they belong to
|
|
4
|
+
* many companies via company_membership); scoping is via the membership, not a column.
|
|
5
|
+
*/
|
|
6
|
+
export declare class User {
|
|
7
|
+
uuid: string;
|
|
8
|
+
username: string;
|
|
9
|
+
email: string;
|
|
10
|
+
status: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=user.object.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user.object.d.ts","sourceRoot":"","sources":["../src/user.object.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,qBAUa,IAAI;IAEf,IAAI,EAAG,MAAM,CAAC;IAGd,QAAQ,EAAG,MAAM,CAAC;IAGlB,KAAK,EAAG,MAAM,CAAC;IAGf,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.User = void 0;
|
|
13
|
+
const engine_1 = require("@astram/engine");
|
|
14
|
+
const schema_1 = require("./schema");
|
|
15
|
+
/**
|
|
16
|
+
* users — the principal. `u_uuid` links to identity_credential.ic_principal_id
|
|
17
|
+
* (the hash lives framework-side). Users are not tenant-stamped (they belong to
|
|
18
|
+
* many companies via company_membership); scoping is via the membership, not a column.
|
|
19
|
+
*/
|
|
20
|
+
let User = class User {
|
|
21
|
+
uuid;
|
|
22
|
+
username;
|
|
23
|
+
email;
|
|
24
|
+
status;
|
|
25
|
+
createdAt;
|
|
26
|
+
};
|
|
27
|
+
exports.User = User;
|
|
28
|
+
__decorate([
|
|
29
|
+
(0, engine_1.Field)({ column: 'u_uuid', type: 'uuid', role: 'primaryKey' }),
|
|
30
|
+
__metadata("design:type", String)
|
|
31
|
+
], User.prototype, "uuid", void 0);
|
|
32
|
+
__decorate([
|
|
33
|
+
(0, engine_1.Field)({ column: 'u_username', type: 'string', required: true, role: 'username' }),
|
|
34
|
+
__metadata("design:type", String)
|
|
35
|
+
], User.prototype, "username", void 0);
|
|
36
|
+
__decorate([
|
|
37
|
+
(0, engine_1.Field)({ column: 'u_email', type: 'string' }),
|
|
38
|
+
__metadata("design:type", String)
|
|
39
|
+
], User.prototype, "email", void 0);
|
|
40
|
+
__decorate([
|
|
41
|
+
(0, engine_1.Field)({ column: 'u_status', type: 'string', role: 'status' }),
|
|
42
|
+
__metadata("design:type", String)
|
|
43
|
+
], User.prototype, "status", void 0);
|
|
44
|
+
__decorate([
|
|
45
|
+
(0, engine_1.Field)({ column: 'u_created_at', type: 'timestamp' }),
|
|
46
|
+
__metadata("design:type", String)
|
|
47
|
+
], User.prototype, "createdAt", void 0);
|
|
48
|
+
exports.User = User = __decorate([
|
|
49
|
+
(0, engine_1.ObjectType)({
|
|
50
|
+
typeKey: 'user',
|
|
51
|
+
table: 'users',
|
|
52
|
+
schema: schema_1.SCHEMA,
|
|
53
|
+
// plane:'control' — `user` is identity administration (the identity-administrator model: the
|
|
54
|
+
// capped root, the identity administrator, may list/read AND create/update/delete users). Uniform
|
|
55
|
+
// across all actions (read reclassified control 2026-07-16), so plane is per-type.
|
|
56
|
+
access: { plane: 'control', system: true },
|
|
57
|
+
}),
|
|
58
|
+
(0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'u_uuid' })
|
|
59
|
+
], User);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework identity migration — the org/identity core in `shield_app`.
|
|
3
|
+
*
|
|
4
|
+
* Creates companies (the tenant), users (the principal; login hash lives in
|
|
5
|
+
* shield_identity.identity_credential), and the global catalogs roles/departments/
|
|
6
|
+
* branches. Final shape — NO legacy junctions (user_companies/user_company_roles/
|
|
7
|
+
* company_roles/user_departments/user_branches are never created; the first-class
|
|
8
|
+
* membership model is created by 1710000000002).
|
|
9
|
+
*
|
|
10
|
+
* Framework-owned (relocated from astram3 migration 1720000000001 + the superuser flag
|
|
11
|
+
* from 1720000000007). Table/column names are read from the @astram/identity object
|
|
12
|
+
* registry so the DDL and runtime queries share one source of truth. Idempotent
|
|
13
|
+
* (IF NOT EXISTS). No hardcoded uuids. Schema is qualified explicitly (shield_app).
|
|
14
|
+
*/
|
|
15
|
+
const { Company, User, Role, Department, Branch } = require('@astram/identity');
|
|
16
|
+
const { columnForRole, reflectObjectType } = require('@astram/engine');
|
|
17
|
+
|
|
18
|
+
const SCHEMA = 'shield_app';
|
|
19
|
+
const TS = 'TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP';
|
|
20
|
+
|
|
21
|
+
// Reflect the @ObjectType/@Field metadata off each decorated class into its registration
|
|
22
|
+
// (`{ typeKey, table, fields, primaryKey, access }`) ONCE — `columnForRole`/`.table` read
|
|
23
|
+
// this registration, not the raw class (the class itself carries no static `.table`).
|
|
24
|
+
const CompanyReg = reflectObjectType(Company);
|
|
25
|
+
const UserReg = reflectObjectType(User);
|
|
26
|
+
const RoleReg = reflectObjectType(Role);
|
|
27
|
+
const DepartmentReg = reflectObjectType(Department);
|
|
28
|
+
const BranchReg = reflectObjectType(Branch);
|
|
29
|
+
|
|
30
|
+
// Resolve a column by role from a reflected object registration (single source of truth).
|
|
31
|
+
const col = (reg, role) => columnForRole(reg, role);
|
|
32
|
+
|
|
33
|
+
async function up({ query }) {
|
|
34
|
+
// companies (the tenant) — hierarchy + type + status.
|
|
35
|
+
await query(`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`${CompanyReg.table}\` (
|
|
36
|
+
${col(CompanyReg, 'primaryKey')} VARCHAR(36) NOT NULL,
|
|
37
|
+
c_name VARCHAR(200) NOT NULL,
|
|
38
|
+
c_parent_uuid VARCHAR(36) NULL,
|
|
39
|
+
c_type VARCHAR(50) NOT NULL DEFAULT 'standard',
|
|
40
|
+
${col(CompanyReg, 'status')} VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
41
|
+
c_created_at ${TS},
|
|
42
|
+
PRIMARY KEY (${col(CompanyReg, 'primaryKey')}),
|
|
43
|
+
INDEX idx_companies_parent (c_parent_uuid),
|
|
44
|
+
INDEX idx_companies_status (${col(CompanyReg, 'status')}),
|
|
45
|
+
CONSTRAINT FK_companies_parent FOREIGN KEY (c_parent_uuid)
|
|
46
|
+
REFERENCES \`${SCHEMA}\`.\`${CompanyReg.table}\` (${col(CompanyReg, 'primaryKey')}) ON DELETE SET NULL
|
|
47
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`);
|
|
48
|
+
|
|
49
|
+
// users (the principal). u_is_platform_superuser is the capped-root source of truth
|
|
50
|
+
// (read by the app policy layer via SQL, not an object field) — created here so the
|
|
51
|
+
// framework owns the column, DEFAULT 0.
|
|
52
|
+
await query(`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`${UserReg.table}\` (
|
|
53
|
+
${col(UserReg, 'primaryKey')} VARCHAR(36) NOT NULL,
|
|
54
|
+
${col(UserReg, 'username')} VARCHAR(150) NOT NULL,
|
|
55
|
+
u_email VARCHAR(255) NULL,
|
|
56
|
+
${col(UserReg, 'status')} VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
57
|
+
u_created_at ${TS},
|
|
58
|
+
u_is_platform_superuser TINYINT(1) NOT NULL DEFAULT 0,
|
|
59
|
+
PRIMARY KEY (${col(UserReg, 'primaryKey')}),
|
|
60
|
+
UNIQUE KEY IDX_users_username (${col(UserReg, 'username')}),
|
|
61
|
+
INDEX idx_users_status (${col(UserReg, 'status')})
|
|
62
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`);
|
|
63
|
+
|
|
64
|
+
// roles — global catalog.
|
|
65
|
+
await query(`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`${RoleReg.table}\` (
|
|
66
|
+
${col(RoleReg, 'primaryKey')} VARCHAR(36) NOT NULL,
|
|
67
|
+
r_name VARCHAR(150) NOT NULL,
|
|
68
|
+
r_created_at ${TS},
|
|
69
|
+
PRIMARY KEY (${col(RoleReg, 'primaryKey')}),
|
|
70
|
+
UNIQUE KEY IDX_roles_name (r_name)
|
|
71
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`);
|
|
72
|
+
|
|
73
|
+
// departments — global catalog.
|
|
74
|
+
await query(`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`${DepartmentReg.table}\` (
|
|
75
|
+
${col(DepartmentReg, 'primaryKey')} VARCHAR(36) NOT NULL,
|
|
76
|
+
d_name VARCHAR(150) NOT NULL,
|
|
77
|
+
d_created_at ${TS},
|
|
78
|
+
PRIMARY KEY (${col(DepartmentReg, 'primaryKey')}),
|
|
79
|
+
UNIQUE KEY IDX_departments_name (d_name)
|
|
80
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`);
|
|
81
|
+
|
|
82
|
+
// branches — global catalog.
|
|
83
|
+
await query(`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`${BranchReg.table}\` (
|
|
84
|
+
${col(BranchReg, 'primaryKey')} VARCHAR(36) NOT NULL,
|
|
85
|
+
b_name VARCHAR(150) NOT NULL,
|
|
86
|
+
b_created_at ${TS},
|
|
87
|
+
PRIMARY KEY (${col(BranchReg, 'primaryKey')}),
|
|
88
|
+
UNIQUE KEY IDX_branches_name (b_name)
|
|
89
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function down({ query }) {
|
|
93
|
+
// Reverse dependency order — membership tables (1710000000002) drop first via their
|
|
94
|
+
// own down(); here drop the catalogs + principal + tenant.
|
|
95
|
+
for (const t of [BranchReg.table, DepartmentReg.table, RoleReg.table, UserReg.table, CompanyReg.table]) {
|
|
96
|
+
await query(`DROP TABLE IF EXISTS \`${SCHEMA}\`.\`${t}\``);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { up, down };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework identity migration — first-class membership + postings + company junctions
|
|
3
|
+
* in `shield_app`. Runs AFTER 1710000000001 (needs users/companies/roles/dept/branch).
|
|
4
|
+
*
|
|
5
|
+
* company_membership — the membership card (active-tenant pointer via cm_is_active).
|
|
6
|
+
* membership_posting — the time-bounded role+dept?+branch? assignment. The effective
|
|
7
|
+
* window is BIGINT epoch-ms (mp_effective_from_ms/until_ms) — final shape, timezone-
|
|
8
|
+
* immune (relocated from astram3 -005 + -050). company_departments/company_branches —
|
|
9
|
+
* tenant-scoped catalog junctions (from -001).
|
|
10
|
+
*
|
|
11
|
+
* Framework-owned. Names from the @astram/identity registry. Idempotent. No uuids.
|
|
12
|
+
*/
|
|
13
|
+
const {
|
|
14
|
+
Company, User, Role, Department, Branch,
|
|
15
|
+
CompanyMembership, MembershipPosting, CompanyDepartment, CompanyBranch,
|
|
16
|
+
} = require('@astram/identity');
|
|
17
|
+
const { columnForRole, reflectObjectType } = require('@astram/engine');
|
|
18
|
+
|
|
19
|
+
const SCHEMA = 'shield_app';
|
|
20
|
+
const TS = 'TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP';
|
|
21
|
+
|
|
22
|
+
// The decorated classes carry NO static `.table` — @ObjectType/@Field metadata is read
|
|
23
|
+
// via reflectObjectType() into a registration ({ typeKey, table, fields, primaryKey }).
|
|
24
|
+
// columnForRole + the table name both read that registration, NOT the raw class. Reflect
|
|
25
|
+
// each once at module scope. (Task 3 hit this: `Company.table` is undefined on the class.)
|
|
26
|
+
const CompanyReg = reflectObjectType(Company);
|
|
27
|
+
const UserReg = reflectObjectType(User);
|
|
28
|
+
const RoleReg = reflectObjectType(Role);
|
|
29
|
+
const DepartmentReg = reflectObjectType(Department);
|
|
30
|
+
const BranchReg = reflectObjectType(Branch);
|
|
31
|
+
const CompanyMembershipReg = reflectObjectType(CompanyMembership);
|
|
32
|
+
const MembershipPostingReg = reflectObjectType(MembershipPosting);
|
|
33
|
+
const CompanyDepartmentReg = reflectObjectType(CompanyDepartment);
|
|
34
|
+
const CompanyBranchReg = reflectObjectType(CompanyBranch);
|
|
35
|
+
const col = (reg, role) => columnForRole(reg, role);
|
|
36
|
+
|
|
37
|
+
async function up({ query }) {
|
|
38
|
+
// company_membership — first-class membership + active-tenant pointer.
|
|
39
|
+
await query(`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`${CompanyMembershipReg.table}\` (
|
|
40
|
+
${col(CompanyMembershipReg, 'primaryKey')} VARCHAR(36) NOT NULL,
|
|
41
|
+
cm_user_uuid VARCHAR(36) NOT NULL,
|
|
42
|
+
cm_company_uuid VARCHAR(36) NOT NULL,
|
|
43
|
+
${col(CompanyMembershipReg, 'active')} TINYINT(1) NOT NULL DEFAULT 0,
|
|
44
|
+
${col(CompanyMembershipReg, 'status')} VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
45
|
+
cm_created_at ${TS},
|
|
46
|
+
cm_created_by VARCHAR(36) NULL,
|
|
47
|
+
PRIMARY KEY (${col(CompanyMembershipReg, 'primaryKey')}),
|
|
48
|
+
UNIQUE KEY IDX_cm_user_company (cm_user_uuid, cm_company_uuid),
|
|
49
|
+
INDEX idx_cm_company (cm_company_uuid),
|
|
50
|
+
INDEX idx_cm_active (cm_user_uuid, ${col(CompanyMembershipReg, 'active')}),
|
|
51
|
+
CONSTRAINT FK_cm_user FOREIGN KEY (cm_user_uuid)
|
|
52
|
+
REFERENCES \`${SCHEMA}\`.\`${UserReg.table}\` (${col(UserReg, 'primaryKey')}) ON DELETE CASCADE,
|
|
53
|
+
CONSTRAINT FK_cm_company FOREIGN KEY (cm_company_uuid)
|
|
54
|
+
REFERENCES \`${SCHEMA}\`.\`${CompanyReg.table}\` (${col(CompanyReg, 'primaryKey')}) ON DELETE CASCADE
|
|
55
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`);
|
|
56
|
+
|
|
57
|
+
// membership_posting — time-bounded role assignment, epoch-ms window (final shape).
|
|
58
|
+
await query(`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`${MembershipPostingReg.table}\` (
|
|
59
|
+
${col(MembershipPostingReg, 'primaryKey')} VARCHAR(36) NOT NULL,
|
|
60
|
+
mp_membership_uuid VARCHAR(36) NOT NULL,
|
|
61
|
+
mp_role_uuid VARCHAR(36) NOT NULL,
|
|
62
|
+
mp_department_uuid VARCHAR(36) NULL,
|
|
63
|
+
mp_branch_uuid VARCHAR(36) NULL,
|
|
64
|
+
${col(MembershipPostingReg, 'effectiveFrom')} BIGINT NULL,
|
|
65
|
+
${col(MembershipPostingReg, 'effectiveUntil')} BIGINT NULL,
|
|
66
|
+
mp_created_at ${TS},
|
|
67
|
+
mp_created_by VARCHAR(36) NULL,
|
|
68
|
+
PRIMARY KEY (${col(MembershipPostingReg, 'primaryKey')}),
|
|
69
|
+
UNIQUE KEY IDX_mp_unique (mp_membership_uuid, mp_role_uuid, mp_department_uuid, mp_branch_uuid),
|
|
70
|
+
INDEX idx_mp_membership (mp_membership_uuid),
|
|
71
|
+
INDEX idx_mp_role (mp_role_uuid),
|
|
72
|
+
CONSTRAINT FK_mp_membership FOREIGN KEY (mp_membership_uuid)
|
|
73
|
+
REFERENCES \`${SCHEMA}\`.\`${CompanyMembershipReg.table}\` (${col(CompanyMembershipReg, 'primaryKey')}) ON DELETE CASCADE,
|
|
74
|
+
CONSTRAINT FK_mp_role FOREIGN KEY (mp_role_uuid)
|
|
75
|
+
REFERENCES \`${SCHEMA}\`.\`${RoleReg.table}\` (${col(RoleReg, 'primaryKey')}) ON DELETE CASCADE,
|
|
76
|
+
CONSTRAINT FK_mp_department FOREIGN KEY (mp_department_uuid)
|
|
77
|
+
REFERENCES \`${SCHEMA}\`.\`${DepartmentReg.table}\` (${col(DepartmentReg, 'primaryKey')}) ON DELETE SET NULL,
|
|
78
|
+
CONSTRAINT FK_mp_branch FOREIGN KEY (mp_branch_uuid)
|
|
79
|
+
REFERENCES \`${SCHEMA}\`.\`${BranchReg.table}\` (${col(BranchReg, 'primaryKey')}) ON DELETE SET NULL
|
|
80
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`);
|
|
81
|
+
|
|
82
|
+
// company_departments — tenant-scoped department catalog junction.
|
|
83
|
+
await query(`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`${CompanyDepartmentReg.table}\` (
|
|
84
|
+
cd_company_uuid VARCHAR(36) NOT NULL,
|
|
85
|
+
cd_department_uuid VARCHAR(36) NOT NULL,
|
|
86
|
+
PRIMARY KEY (cd_company_uuid, cd_department_uuid),
|
|
87
|
+
CONSTRAINT FK_cd_company FOREIGN KEY (cd_company_uuid)
|
|
88
|
+
REFERENCES \`${SCHEMA}\`.\`${CompanyReg.table}\` (${col(CompanyReg, 'primaryKey')}) ON DELETE CASCADE,
|
|
89
|
+
CONSTRAINT FK_cd_department FOREIGN KEY (cd_department_uuid)
|
|
90
|
+
REFERENCES \`${SCHEMA}\`.\`${DepartmentReg.table}\` (${col(DepartmentReg, 'primaryKey')}) ON DELETE CASCADE
|
|
91
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`);
|
|
92
|
+
|
|
93
|
+
// company_branches — tenant-scoped branch catalog junction.
|
|
94
|
+
await query(`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`${CompanyBranchReg.table}\` (
|
|
95
|
+
cb_company_uuid VARCHAR(36) NOT NULL,
|
|
96
|
+
cb_branch_uuid VARCHAR(36) NOT NULL,
|
|
97
|
+
PRIMARY KEY (cb_company_uuid, cb_branch_uuid),
|
|
98
|
+
CONSTRAINT FK_cb_company FOREIGN KEY (cb_company_uuid)
|
|
99
|
+
REFERENCES \`${SCHEMA}\`.\`${CompanyReg.table}\` (${col(CompanyReg, 'primaryKey')}) ON DELETE CASCADE,
|
|
100
|
+
CONSTRAINT FK_cb_branch FOREIGN KEY (cb_branch_uuid)
|
|
101
|
+
REFERENCES \`${SCHEMA}\`.\`${BranchReg.table}\` (${col(BranchReg, 'primaryKey')}) ON DELETE CASCADE
|
|
102
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function down({ query }) {
|
|
106
|
+
for (const t of [MembershipPostingReg.table, CompanyBranchReg.table, CompanyDepartmentReg.table, CompanyMembershipReg.table]) {
|
|
107
|
+
await query(`DROP TABLE IF EXISTS \`${SCHEMA}\`.\`${t}\``);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { up, down };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework identity migration — the identity_credential table (the login
|
|
3
|
+
* credential store) in `shield_identity`.
|
|
4
|
+
*
|
|
5
|
+
* Relocates the DDL from the retired Docker init-SQL file
|
|
6
|
+
* `infra/mariadb/init/30-identity-credential.sql` into a proper framework
|
|
7
|
+
* migration. `shield_identity` is a CORE security schema, NOT shield_app —
|
|
8
|
+
* login credentials are authentication data, structurally out of reach of the
|
|
9
|
+
* application accounts (ADR-0014). `shield_core` already holds DML
|
|
10
|
+
* (SELECT/INSERT/UPDATE/DELETE) on shield_identity (10-accounts-grants.sql),
|
|
11
|
+
* so the MariaDbCredentialResolver can SELECT this table with no new grant.
|
|
12
|
+
*
|
|
13
|
+
* ⛔ SECURITY: only the Argon2id ENCODED hash is stored — never a plaintext
|
|
14
|
+
* password. TOTP secrets are NOT stored here — the multi-device MFA model
|
|
15
|
+
* keeps each authenticator's secret in `shield_identity.mfa_device` (created
|
|
16
|
+
* by 1710000000005).
|
|
17
|
+
*
|
|
18
|
+
* This migration carries the CREATE TABLE only — the alice/bob/carol
|
|
19
|
+
* test-fixture seed rows that lived alongside this DDL in the init-SQL file
|
|
20
|
+
* are NOT reproduced here; they are re-homed as a dev-only seed in a later
|
|
21
|
+
* migration.
|
|
22
|
+
*
|
|
23
|
+
* NOT in the @astram/identity object registry (engine-internal auth table) —
|
|
24
|
+
* literal column names, no reflectObjectType, no @astram/identity import.
|
|
25
|
+
* Idempotent (CREATE TABLE IF NOT EXISTS).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const SCHEMA = 'shield_identity';
|
|
29
|
+
|
|
30
|
+
async function up({ query }) {
|
|
31
|
+
// identity_credential — one row per login principal. Resolved by
|
|
32
|
+
// ic_identity_hint (username/email) at login. ic_password_hash is the
|
|
33
|
+
// Argon2id encoded hash the engine verifies against. Whether the principal
|
|
34
|
+
// must also satisfy a TOTP second factor is derived from their CONFIRMED
|
|
35
|
+
// shield_identity.mfa_device rows (multi-device MFA), not a column here.
|
|
36
|
+
await query(
|
|
37
|
+
`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`identity_credential\` (
|
|
38
|
+
\`ic_identity_hint\` VARCHAR(255) NOT NULL,
|
|
39
|
+
\`ic_password_hash\` VARCHAR(255) NOT NULL,
|
|
40
|
+
\`ic_principal_id\` VARCHAR(255) NOT NULL,
|
|
41
|
+
\`ic_tenant_id\` VARCHAR(255) NOT NULL,
|
|
42
|
+
\`ic_roles\` JSON NOT NULL,
|
|
43
|
+
\`ic_created_at_ms\` BIGINT NOT NULL,
|
|
44
|
+
\`ic_updated_at_ms\` BIGINT NOT NULL,
|
|
45
|
+
PRIMARY KEY (\`ic_identity_hint\`),
|
|
46
|
+
-- The login lookup is \`WHERE ic_identity_hint = ? LIMIT 1\`; the PK already
|
|
47
|
+
-- makes the hint the unique lookup key. A redundant UNIQUE index is stated
|
|
48
|
+
-- explicitly for the contract's "UNIQUE on ic_identity_hint" invariant.
|
|
49
|
+
UNIQUE KEY \`IDX_identity_credential_hint\` (\`ic_identity_hint\`)
|
|
50
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function down({ query }) {
|
|
55
|
+
await query(`DROP TABLE IF EXISTS \`${SCHEMA}\`.\`identity_credential\``);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { up, down };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework identity migration — durable SESSION + REFRESH-TOKEN tables
|
|
3
|
+
* (ADR-0013 hybrid storage, ADR-0015 lifetimes + rotation lineage + reuse
|
|
4
|
+
* detection) in `shield_identity`.
|
|
5
|
+
*
|
|
6
|
+
* Relocates the DDL from the retired Docker init-SQL file
|
|
7
|
+
* `infra/mariadb/init/20-session-tables.sql` into a proper framework
|
|
8
|
+
* migration. `shield_identity` is a CORE security schema, NOT shield_app —
|
|
9
|
+
* session/refresh state is authentication data, structurally out of reach of
|
|
10
|
+
* the application accounts (ADR-0014). `shield_core` already holds DML on
|
|
11
|
+
* shield_identity (10-accounts-grants.sql), so these tables are
|
|
12
|
+
* readable/writable by the Session engine's MariaDbSessionRecordStore with no
|
|
13
|
+
* new grant.
|
|
14
|
+
*
|
|
15
|
+
* ⛔ SECURITY: the refresh token itself is NEVER stored — only its keyed HASH
|
|
16
|
+
* (HMAC-SHA-256, hashed by the app before insert). rt_token_hash holds the hash.
|
|
17
|
+
*
|
|
18
|
+
* `session` is created FIRST — `refresh_token` carries FK_refresh_token_session
|
|
19
|
+
* referencing session(s_session_id), so the parent must exist before the child.
|
|
20
|
+
*
|
|
21
|
+
* NOT in the @astram/identity object registry (engine-internal auth tables) —
|
|
22
|
+
* literal column names, no reflectObjectType, no @astram/identity import.
|
|
23
|
+
* Idempotent (CREATE TABLE IF NOT EXISTS).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const SCHEMA = 'shield_identity';
|
|
27
|
+
|
|
28
|
+
async function up({ query }) {
|
|
29
|
+
// session — the authoritative session record (written at login; read/updated
|
|
30
|
+
// at refresh + revocation). s_absolute_expires_at_ms anchors the ADR-0015
|
|
31
|
+
// absolute cap (from login, never reset). Status ∈ active|rotated|revoked|expired.
|
|
32
|
+
await query(
|
|
33
|
+
`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`session\` (
|
|
34
|
+
\`s_session_id\` CHAR(64) NOT NULL,
|
|
35
|
+
\`s_principal_id\` VARCHAR(255) NOT NULL,
|
|
36
|
+
\`s_tenant_id\` VARCHAR(255) NOT NULL,
|
|
37
|
+
\`s_status\` ENUM('active','rotated','revoked','expired') NOT NULL DEFAULT 'active',
|
|
38
|
+
\`s_issued_at_ms\` BIGINT NOT NULL,
|
|
39
|
+
\`s_absolute_expires_at_ms\` BIGINT NOT NULL,
|
|
40
|
+
PRIMARY KEY (\`s_session_id\`),
|
|
41
|
+
KEY \`IDX_session_principal\` (\`s_tenant_id\`, \`s_principal_id\`),
|
|
42
|
+
KEY \`IDX_session_status\` (\`s_status\`)
|
|
43
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// refresh_token — one row per minted refresh token, chained by rotation lineage
|
|
47
|
+
// (ADR-0013/0015). rt_token_hash = the KEYED HASH (never the raw token).
|
|
48
|
+
// rt_lineage_id chains every rotation of one login; rt_prev_refresh_id points at
|
|
49
|
+
// the record this one superseded (NULL for the first). Reuse detection = a lookup
|
|
50
|
+
// by hash that finds a NON-active row → revoke the whole rt_lineage_id.
|
|
51
|
+
await query(
|
|
52
|
+
`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`refresh_token\` (
|
|
53
|
+
\`rt_refresh_id\` CHAR(64) NOT NULL,
|
|
54
|
+
\`rt_session_id\` CHAR(64) NOT NULL,
|
|
55
|
+
\`rt_lineage_id\` CHAR(64) NOT NULL,
|
|
56
|
+
\`rt_prev_refresh_id\` CHAR(64) NULL,
|
|
57
|
+
\`rt_token_hash\` CHAR(64) NOT NULL,
|
|
58
|
+
\`rt_status\` ENUM('active','rotated','revoked','expired') NOT NULL DEFAULT 'active',
|
|
59
|
+
\`rt_issued_at_ms\` BIGINT NOT NULL,
|
|
60
|
+
\`rt_idle_expires_at_ms\` BIGINT NOT NULL,
|
|
61
|
+
PRIMARY KEY (\`rt_refresh_id\`),
|
|
62
|
+
-- Lookup by hash is the hot rotate path; unique so one hash resolves one record.
|
|
63
|
+
UNIQUE KEY \`IDX_refresh_token_hash\` (\`rt_token_hash\`),
|
|
64
|
+
KEY \`IDX_refresh_lineage\` (\`rt_lineage_id\`),
|
|
65
|
+
KEY \`IDX_refresh_session\` (\`rt_session_id\`),
|
|
66
|
+
CONSTRAINT \`FK_refresh_token_session\`
|
|
67
|
+
FOREIGN KEY (\`rt_session_id\`) REFERENCES \`${SCHEMA}\`.\`session\` (\`s_session_id\`)
|
|
68
|
+
ON DELETE CASCADE
|
|
69
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function down({ query }) {
|
|
74
|
+
// Child before parent — refresh_token FK-references session.
|
|
75
|
+
await query(`DROP TABLE IF EXISTS \`${SCHEMA}\`.\`refresh_token\``);
|
|
76
|
+
await query(`DROP TABLE IF EXISTS \`${SCHEMA}\`.\`session\``);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { up, down };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework identity migration — the MFA-device + forgot/reset + MFA-recovery
|
|
3
|
+
* tables in `shield_identity`: mfa_device, password_reset_token,
|
|
4
|
+
* mfa_recovery_code.
|
|
5
|
+
*
|
|
6
|
+
* Relocates the DDL from astram3 migrations 1720000000004
|
|
7
|
+
* (CreateMfaDeviceTable) and 1720000000003 (CreateCredentialSurfaceTables)
|
|
8
|
+
* into framework migrations. All three live in the shield_identity credential
|
|
9
|
+
* schema (ADR-0014) alongside identity_credential: device secrets,
|
|
10
|
+
* reset-token hashes, and recovery-code hashes are authentication data that
|
|
11
|
+
* belongs in the secure framework schema, not the app's org tables. The
|
|
12
|
+
* runtime shield_core account has DML there, so the consumer's port impls
|
|
13
|
+
* (MfaDeviceStore / ResetTokenStore / RecoveryCodeStore) INSERT/UPDATE/DELETE
|
|
14
|
+
* with no extra grant.
|
|
15
|
+
*
|
|
16
|
+
* Columns:
|
|
17
|
+
* - mfa_device: one row per enrolled authenticator device. md_secret is this
|
|
18
|
+
* device's OWN base32 TOTP secret (each device has a distinct secret; login
|
|
19
|
+
* matches ANY confirmed device via the consumer's OR-verifier).
|
|
20
|
+
* md_confirmed_at_ms NULL = pending (never satisfies login, self-pruned on
|
|
21
|
+
* the next begin); set = active. md_last_used_at_ms best-effort stamped
|
|
22
|
+
* when a code from this device passes. PK on md_id; index by principal for
|
|
23
|
+
* the per-user list/verify.
|
|
24
|
+
* - password_reset_token: framework hands a SHA-256 HASH of the raw token
|
|
25
|
+
* (never the raw token); TTL enforced on consume via prt_expires_at_ms;
|
|
26
|
+
* single-use marked by prt_consumed_at_ms. PK on the hash (one row per
|
|
27
|
+
* issued token); index by principal for invalidate-all.
|
|
28
|
+
* - mfa_recovery_code: one row per Argon2-hashed one-time recovery code; PK
|
|
29
|
+
* (principal, hash) so replaceAll wipes-then-inserts cleanly; single-use
|
|
30
|
+
* marked by mrc_consumed_at_ms.
|
|
31
|
+
*
|
|
32
|
+
* This clean DDL never created the retired single-secret `ic_totp_secret`
|
|
33
|
+
* column on identity_credential (the multi-device model stores every TOTP
|
|
34
|
+
* secret in mfa_device from the start here), so — unlike the astram3 source
|
|
35
|
+
* migration — there is no DROP COLUMN step to reproduce.
|
|
36
|
+
*
|
|
37
|
+
* NOT in the @astram/identity object registry (engine-internal auth tables) —
|
|
38
|
+
* literal column names, no reflectObjectType, no @astram/identity import.
|
|
39
|
+
* Idempotent (CREATE TABLE IF NOT EXISTS).
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const SCHEMA = 'shield_identity';
|
|
43
|
+
|
|
44
|
+
async function up({ query }) {
|
|
45
|
+
await query(
|
|
46
|
+
`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`mfa_device\` (
|
|
47
|
+
md_id VARCHAR(36) NOT NULL,
|
|
48
|
+
md_principal_id VARCHAR(255) NOT NULL,
|
|
49
|
+
md_name VARCHAR(120) NOT NULL,
|
|
50
|
+
md_secret VARCHAR(255) NOT NULL,
|
|
51
|
+
md_created_at_ms BIGINT NOT NULL,
|
|
52
|
+
md_last_used_at_ms BIGINT NULL,
|
|
53
|
+
md_confirmed_at_ms BIGINT NULL,
|
|
54
|
+
PRIMARY KEY (md_id),
|
|
55
|
+
INDEX idx_md_principal (md_principal_id)
|
|
56
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
await query(
|
|
60
|
+
`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`password_reset_token\` (
|
|
61
|
+
prt_token_hash VARCHAR(255) NOT NULL,
|
|
62
|
+
prt_principal_id VARCHAR(255) NOT NULL,
|
|
63
|
+
prt_expires_at_ms BIGINT NOT NULL,
|
|
64
|
+
prt_consumed_at_ms BIGINT NULL,
|
|
65
|
+
prt_created_at_ms BIGINT NOT NULL,
|
|
66
|
+
PRIMARY KEY (prt_token_hash),
|
|
67
|
+
INDEX idx_prt_principal (prt_principal_id)
|
|
68
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
await query(
|
|
72
|
+
`CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`mfa_recovery_code\` (
|
|
73
|
+
mrc_principal_id VARCHAR(255) NOT NULL,
|
|
74
|
+
mrc_code_hash VARCHAR(255) NOT NULL,
|
|
75
|
+
mrc_consumed_at_ms BIGINT NULL,
|
|
76
|
+
mrc_created_at_ms BIGINT NOT NULL,
|
|
77
|
+
PRIMARY KEY (mrc_principal_id, mrc_code_hash)
|
|
78
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function down({ query }) {
|
|
83
|
+
for (const t of ['mfa_recovery_code', 'password_reset_token', 'mfa_device']) {
|
|
84
|
+
await query(`DROP TABLE IF EXISTS \`${SCHEMA}\`.\`${t}\``);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { up, down };
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@astram/identity",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Astram framework — the framework-owned identity + tenancy model. Hosts the identity @ObjectType classes (user, company, membership, posting), framework-authored identity migrations, and the identity resolver. Top of the dependency graph: depends on @astram/engine (for @ObjectType/@Field decorators) + @astram/guard / @astram/core (for the auth ports).",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"migrations"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@astram/core": "1.0.0",
|
|
25
|
+
"@astram/engine": "0.1.0",
|
|
26
|
+
"@astram/guard": "0.3.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@nestjs/common": "^11",
|
|
30
|
+
"@nestjs/core": "^11",
|
|
31
|
+
"reflect-metadata": "^0.2",
|
|
32
|
+
"rxjs": "^7"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@nestjs/common": "^11.0.0",
|
|
36
|
+
"@nestjs/core": "^11.0.0",
|
|
37
|
+
"@nestjs/testing": "^11.0.0",
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"reflect-metadata": "^0.2.2",
|
|
40
|
+
"rxjs": "^7.8.1",
|
|
41
|
+
"typescript": "^5.7.0",
|
|
42
|
+
"unplugin-swc": "^1.5.1",
|
|
43
|
+
"vitest": "^2.1.8"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
|
+
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
48
|
+
"test": "vitest run"
|
|
49
|
+
}
|
|
50
|
+
}
|