@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.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/dist/modules/identity/identity.administration-migration.d.ts +2 -0
- package/dist/modules/identity/identity.administration-migration.js +75 -0
- package/dist/modules/identity/identity.administration-repository.d.ts +19 -0
- package/dist/modules/identity/identity.administration-repository.js +102 -0
- package/dist/modules/identity/identity.administration-schema.d.ts +113 -0
- package/dist/modules/identity/identity.administration-schema.js +121 -0
- package/dist/modules/identity/identity.administration-service.d.ts +97 -0
- package/dist/modules/identity/identity.administration-service.js +517 -0
- package/dist/modules/identity/identity.controller.d.ts +22 -0
- package/dist/modules/identity/identity.controller.js +278 -0
- package/dist/modules/identity/identity.error.d.ts +6 -0
- package/dist/modules/identity/identity.error.js +15 -0
- package/dist/modules/identity/identity.lifecycle-schema.d.ts +19 -0
- package/dist/modules/identity/identity.lifecycle-schema.js +29 -0
- package/dist/modules/identity/identity.lifecycle-service.d.ts +64 -0
- package/dist/modules/identity/identity.lifecycle-service.js +395 -0
- package/dist/modules/identity/identity.membership-policy.d.ts +3 -0
- package/dist/modules/identity/identity.membership-policy.js +27 -0
- package/dist/modules/identity/identity.migration.d.ts +2 -0
- package/dist/modules/identity/identity.migration.js +79 -0
- package/dist/modules/identity/identity.password.d.ts +3 -0
- package/dist/modules/identity/identity.password.js +21 -0
- package/dist/modules/identity/identity.permission-declarations-migration.d.ts +5 -0
- package/dist/modules/identity/identity.permission-declarations-migration.js +15 -0
- package/dist/modules/identity/identity.permission-declarations.d.ts +23 -0
- package/dist/modules/identity/identity.permission-declarations.js +57 -0
- package/dist/modules/identity/identity.permission-labels-migration.d.ts +5 -0
- package/dist/modules/identity/identity.permission-labels-migration.js +9 -0
- package/dist/modules/identity/identity.presentation-service.d.ts +23 -0
- package/dist/modules/identity/identity.presentation-service.js +51 -0
- package/dist/modules/identity/identity.provider.d.ts +78 -0
- package/dist/modules/identity/identity.provider.js +119 -0
- package/dist/modules/identity/identity.public-schemas.d.ts +4 -0
- package/dist/modules/identity/identity.public-schemas.js +3 -0
- package/dist/modules/identity/identity.repository.d.ts +40 -0
- package/dist/modules/identity/identity.repository.js +196 -0
- package/dist/modules/identity/identity.request-context.d.ts +2 -0
- package/dist/modules/identity/identity.request-context.js +10 -0
- package/dist/modules/identity/identity.roles-migration.d.ts +2 -0
- package/dist/modules/identity/identity.roles-migration.js +64 -0
- package/dist/modules/identity/identity.roles-service.d.ts +49 -0
- package/dist/modules/identity/identity.roles-service.js +327 -0
- package/dist/modules/identity/identity.routes.d.ts +22 -0
- package/dist/modules/identity/identity.routes.js +11 -0
- package/dist/modules/identity/identity.schema.d.ts +27 -0
- package/dist/modules/identity/identity.schema.js +34 -0
- package/dist/modules/identity/identity.seed.d.ts +3 -0
- package/dist/modules/identity/identity.seed.js +81 -0
- package/dist/modules/identity/identity.service.d.ts +32 -0
- package/dist/modules/identity/identity.service.js +83 -0
- package/dist/modules/identity/identity.types.d.ts +150 -0
- package/dist/modules/identity/identity.types.js +1 -0
- package/dist/modules/identity/index.d.ts +12 -0
- package/dist/modules/identity/index.js +9 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Devxcrew
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Platform Core
|
|
2
|
+
|
|
3
|
+
Source repository: https://github.com/devxcrew/platform.
|
|
4
|
+
Package: `@devxcrew/platform`, version `0.1.0`.
|
|
5
|
+
|
|
6
|
+
Platform Core owns database identity, sessions, role permissions, and tenant membership.
|
|
7
|
+
Applications consume its public provider. Framework remains responsible for business-neutral runtime primitives.
|
|
8
|
+
|
|
9
|
+
Public exports include `createIdentityProvider`, `identityMigration`, `seedIdentity`, and identity contract types.
|
|
10
|
+
The application provides its Kysely connection and validated environment. Platform Core owns all identity SQL and rules.
|
|
11
|
+
|
|
12
|
+
## Identity foundation
|
|
13
|
+
|
|
14
|
+
Three portal identities are supported: user, admin, and super-admin.
|
|
15
|
+
Each portal requires an explicit tenant membership and its own desk permission.
|
|
16
|
+
Administrator and super-administrator sessions do not grant access to other portals implicitly.
|
|
17
|
+
|
|
18
|
+
Passwords use salted scrypt with N=32768, r=8, and p=3.
|
|
19
|
+
Opaque session tokens use 32 random bytes. Only SHA-256 token hashes are stored in the database.
|
|
20
|
+
Cookies are app- and portal-specific, HttpOnly, and SameSite=Strict.
|
|
21
|
+
HTTPS uses Secure and the __Host- cookie prefix. HTTP is allowed only on loopback hosts.
|
|
22
|
+
Unsafe HTTP requests require the configured application Origin. Login attempts have persistent per-address and per-account limits.
|
|
23
|
+
|
|
24
|
+
Single-client mode resolves the configured tenant on the server.
|
|
25
|
+
Multi-tenant mode accepts a tenant ID only after verifying database membership.
|
|
26
|
+
Every session lookup rechecks account status, tenant status, membership, and current permissions.
|
|
27
|
+
Password changes revoke every session belonging to the account.
|
|
28
|
+
|
|
29
|
+
## Seed behavior
|
|
30
|
+
|
|
31
|
+
Seeds create the configured organization, three roles, and foundation permissions.
|
|
32
|
+
Optional bootstrap accounts require explicit email and password environment values.
|
|
33
|
+
There are no default passwords. Existing users and memberships are not elevated or overwritten by the seed.
|
|
34
|
+
|
|
35
|
+
## Development
|
|
36
|
+
|
|
37
|
+
Run `npm install`, `npm test`, and `npm run build` in this package.
|
|
38
|
+
Retrieve workspace instructions with this owner's authenticated npm run mcp:connect before edits.
|
|
39
|
+
The package name is `@devxcrew/platform`. Cxsun consumes a user-approved bundled development snapshot.
|
|
40
|
+
Owner authentication works. Deployed repository metadata remains pending. This package is not published to npm.
|
|
41
|
+
|
|
42
|
+
Invitation and recovery APIs support injected delivery. Real provider delivery remains a release gate.
|
|
43
|
+
Administration resources and settings are implemented. MFA and email verification remain explicit policy decisions.
|
|
44
|
+
This foundation does not claim production security certification or enterprise operational readiness.
|
|
45
|
+
|
|
46
|
+
## Owner checks
|
|
47
|
+
|
|
48
|
+
Run npm run release:check for tests, build, dependency order, version alignment, LF and dry-run package checks.
|
|
49
|
+
Maintenance uses the installed public @devxcrew/tools package. agent/CHANGELOG.md owns release metadata.
|
|
50
|
+
Use version-bump, fix:line-endings, lines:check and check:versions before a coordinated release.
|
|
51
|
+
Publication, Git delivery and deployment require authorization.
|
|
52
|
+
|
|
53
|
+
## Module permission extensions
|
|
54
|
+
|
|
55
|
+
Use the public identity.registerPermissions API or provider permissions options.
|
|
56
|
+
Register identityPermissionDeclarationsMigration after the roles migration. verify registers option declarations.
|
|
57
|
+
IDs use APP_ID.owner.action and declared allowed portals. Registration grants no roles automatically.
|
|
58
|
+
See agent/PERMISSION-EXTENSIONS.md for the complete contract and local evidence.
|
|
59
|
+
|
|
60
|
+
Module controllers use identity.authenticateRequest(request, portal, signal), then identity.requirePermission.
|
|
61
|
+
Platform owns portal cookies, unsafe Origin validation and cancellation. Raw-token authenticate remains supported.
|
|
62
|
+
|
|
63
|
+
Permission declarations may include an owner-written label. Permission catalog DTOs expose id, label, owner, appId and portals.
|
|
64
|
+
Register identityPermissionLabelsMigration after declarations. Filter role choices by allowed portals and retain server validation.
|
|
65
|
+
|
|
66
|
+
## Identity delivery failure contract
|
|
67
|
+
|
|
68
|
+
Platform owns invitation and recovery tokens. Email owns transport and receipt validation.
|
|
69
|
+
A successful send must return a nonempty receipt before the token becomes usable.
|
|
70
|
+
A transport error, timeout, canceled request, or missing receipt removes the newly issued token and returns a safe unavailable error.
|
|
71
|
+
A provider can accept a message before its response times out. That message can contain an unusable link.
|
|
72
|
+
The UI must offer a new explicit request after failure. It must not promise delivery or retry silently.
|
|
73
|
+
Successful resend creates a new token, then revokes the old invitation. A failed resend leaves the old invitation available.
|
|
74
|
+
Recovery requests invalidate earlier recovery tokens for the same account.
|
|
75
|
+
Recovery links expire after 30 minutes. Invitations expire after 24 hours. Completion claims one token once.
|
|
76
|
+
Disabled delivery returns unavailable and does not create a simulated success.
|
|
77
|
+
No automatic retry queue or durable outbox is implemented in this profile.
|
|
78
|
+
Actual SMTP delivery acceptance remains deferred. Local failure and concurrency checks do not establish real recipient delivery.
|
|
79
|
+
|
|
80
|
+
## Distribution
|
|
81
|
+
|
|
82
|
+
First-party package code uses the MIT license. Dependency licenses and notices retain their original terms.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createIdentityProvider, IdentityError, identityMigration, identityAdministrationMigration, identityRolesMigration, identityPermissionDeclarationsMigration, identityPermissionLabelsMigration, seedIdentity, } from "./modules/identity/index.js";
|
|
2
|
+
export type { IdentityPermissionDeclaration } from "./modules/identity/index.js";
|
|
3
|
+
export { listSchema, userCreateSchema, userUpdateSchema, organizationCreateSchema, organizationUpdateSchema, membershipSchema, roleUpdateSchema, profileSchema, settingsSchema, } from "./modules/identity/index.js";
|
|
4
|
+
export type { IdentityListQuery, IdentityResource, } from "./modules/identity/index.js";
|
|
5
|
+
export type { IdentitySchema, IdentityConfig, Principal, Portal, IdentityDeliveryProvider, IdentityProviderOptions, IdentityPermissionCatalogEntry, } from "./modules/identity/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { createIdentityProvider, IdentityError, identityMigration, identityAdministrationMigration, identityRolesMigration, identityPermissionDeclarationsMigration, identityPermissionLabelsMigration, seedIdentity, } from "./modules/identity/index.js";
|
|
2
|
+
export { listSchema, userCreateSchema, userUpdateSchema, organizationCreateSchema, organizationUpdateSchema, membershipSchema, roleUpdateSchema, profileSchema, settingsSchema, } from "./modules/identity/index.js";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const identityAdministrationMigration = {
|
|
2
|
+
async up(db) {
|
|
3
|
+
for (const table of [
|
|
4
|
+
"identity_users",
|
|
5
|
+
"identity_tenants",
|
|
6
|
+
"identity_roles",
|
|
7
|
+
])
|
|
8
|
+
await db.schema
|
|
9
|
+
.alterTable(table)
|
|
10
|
+
.addColumn("version", "integer", (c) => c.notNull().defaultTo(0))
|
|
11
|
+
.execute();
|
|
12
|
+
await db.schema
|
|
13
|
+
.createTable("identity_app_settings")
|
|
14
|
+
.addColumn("app_id", "text", (c) => c.primaryKey())
|
|
15
|
+
.addColumn("display_name", "text", (c) => c.notNull())
|
|
16
|
+
.addColumn("locale", "text", (c) => c.notNull().defaultTo("en"))
|
|
17
|
+
.addColumn("time_zone", "text", (c) => c.notNull().defaultTo("UTC"))
|
|
18
|
+
.addColumn("session_seconds", "integer", (c) => c.notNull().defaultTo(28800))
|
|
19
|
+
.execute();
|
|
20
|
+
await db.schema
|
|
21
|
+
.alterTable("identity_app_settings")
|
|
22
|
+
.addColumn("version", "integer", (c) => c.notNull().defaultTo(0))
|
|
23
|
+
.execute();
|
|
24
|
+
await db.schema
|
|
25
|
+
.createTable("identity_tokens")
|
|
26
|
+
.addColumn("id", "text", (c) => c.primaryKey())
|
|
27
|
+
.addColumn("token_hash", "text", (c) => c.notNull().unique())
|
|
28
|
+
.addColumn("app_id", "text", (c) => c.notNull())
|
|
29
|
+
.addColumn("tenant_id", "text", (c) => c.notNull().references("identity_tenants.id"))
|
|
30
|
+
.addColumn("kind", "text", (c) => c.notNull())
|
|
31
|
+
.addColumn("email", "text", (c) => c.notNull())
|
|
32
|
+
.addColumn("name", "text", (c) => c.notNull())
|
|
33
|
+
.addColumn("role_id", "text", (c) => c.notNull().references("identity_roles.id"))
|
|
34
|
+
.addColumn("user_id", "text")
|
|
35
|
+
.addColumn("expires_at", "text", (c) => c.notNull())
|
|
36
|
+
.addColumn("consumed_at", "text")
|
|
37
|
+
.addColumn("delivered", "integer", (c) => c.notNull().defaultTo(0))
|
|
38
|
+
.execute();
|
|
39
|
+
await db.schema
|
|
40
|
+
.createTable("identity_settings")
|
|
41
|
+
.addColumn("tenant_id", "text", (c) => c.primaryKey().references("identity_tenants.id").onDelete("cascade"))
|
|
42
|
+
.addColumn("display_name", "text", (c) => c.notNull())
|
|
43
|
+
.addColumn("locale", "text", (c) => c.notNull().defaultTo("en"))
|
|
44
|
+
.addColumn("time_zone", "text", (c) => c.notNull().defaultTo("UTC"))
|
|
45
|
+
.addColumn("version", "integer", (c) => c.notNull().defaultTo(0))
|
|
46
|
+
.execute();
|
|
47
|
+
await db.schema
|
|
48
|
+
.createTable("identity_audit_events")
|
|
49
|
+
.addColumn("id", "text", (c) => c.primaryKey())
|
|
50
|
+
.addColumn("app_id", "text", (c) => c.notNull())
|
|
51
|
+
.addColumn("actor_id", "text", (c) => c.notNull())
|
|
52
|
+
.addColumn("tenant_id", "text", (c) => c.notNull())
|
|
53
|
+
.addColumn("action", "text", (c) => c.notNull())
|
|
54
|
+
.addColumn("resource_id", "text", (c) => c.notNull())
|
|
55
|
+
.addColumn("created_at", "text", (c) => c.notNull())
|
|
56
|
+
.execute();
|
|
57
|
+
await db.schema
|
|
58
|
+
.createIndex("identity_audit_scope")
|
|
59
|
+
.on("identity_audit_events")
|
|
60
|
+
.columns(["app_id", "tenant_id", "created_at"])
|
|
61
|
+
.execute();
|
|
62
|
+
},
|
|
63
|
+
async down(db) {
|
|
64
|
+
for (const table of [
|
|
65
|
+
"identity_users",
|
|
66
|
+
"identity_tenants",
|
|
67
|
+
"identity_roles",
|
|
68
|
+
])
|
|
69
|
+
await db.schema.alterTable(table).dropColumn("version").execute();
|
|
70
|
+
await db.schema.dropTable("identity_app_settings").execute();
|
|
71
|
+
await db.schema.dropTable("identity_tokens").execute();
|
|
72
|
+
await db.schema.dropTable("identity_audit_events").execute();
|
|
73
|
+
await db.schema.dropTable("identity_settings").execute();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Kysely } from "kysely";
|
|
2
|
+
import type { IdentitySchema, Principal } from "./identity.types.js";
|
|
3
|
+
import type { IdentityListQuery, IdentityResource } from "./identity.administration-schema.js";
|
|
4
|
+
export declare class IdentityAdministrationRepository {
|
|
5
|
+
private readonly db;
|
|
6
|
+
constructor(db: Kysely<IdentitySchema>);
|
|
7
|
+
list(actor: Principal, resource: IdentityResource, query: IdentityListQuery): Promise<{
|
|
8
|
+
data: Record<string, unknown>[];
|
|
9
|
+
meta: {
|
|
10
|
+
page: number;
|
|
11
|
+
perPage: number;
|
|
12
|
+
total: number;
|
|
13
|
+
lastPage: number;
|
|
14
|
+
};
|
|
15
|
+
}>;
|
|
16
|
+
show(actor: Principal, resource: IdentityResource, id: string): Promise<Record<string, unknown> | undefined>;
|
|
17
|
+
private source;
|
|
18
|
+
private present;
|
|
19
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
2
|
+
import { IdentityError } from "./identity.error.js";
|
|
3
|
+
export class IdentityAdministrationRepository {
|
|
4
|
+
db;
|
|
5
|
+
constructor(db) {
|
|
6
|
+
this.db = db;
|
|
7
|
+
}
|
|
8
|
+
async list(actor, resource, query) {
|
|
9
|
+
const source = this.source(actor, resource);
|
|
10
|
+
const fields = resource === "users"
|
|
11
|
+
? ["id", "name", "email"]
|
|
12
|
+
: resource === "organizations" || resource === "roles"
|
|
13
|
+
? ["id", "name"]
|
|
14
|
+
: resource === "memberships"
|
|
15
|
+
? ["id", "userName", "organizationName", "roleName"]
|
|
16
|
+
: ["id"];
|
|
17
|
+
if (!fields.includes(query.sort))
|
|
18
|
+
throw new IdentityError(422, "This sort field is not supported for this resource.");
|
|
19
|
+
const search = query.search
|
|
20
|
+
? sql `where ${sql.join(fields.map((field) => sql `lower(cast(${sql.ref(field)} as text)) like ${`%${query.search.toLowerCase()}%`}`), sql ` or `)}`
|
|
21
|
+
: sql ``;
|
|
22
|
+
const count = await sql `select count(*) as total from (${source}) as resources ${search}`.execute(this.db);
|
|
23
|
+
const sort = query.sort;
|
|
24
|
+
const rows = await sql `select * from (${source}) as resources ${search}
|
|
25
|
+
order by ${sql.ref(sort)} ${query.direction === "asc" ? sql `asc` : sql `desc`}, id asc
|
|
26
|
+
limit ${query.perPage} offset ${(query.page - 1) * query.perPage}`.execute(this.db);
|
|
27
|
+
const total = Number(count.rows[0].total);
|
|
28
|
+
return {
|
|
29
|
+
data: rows.rows.map((row) => this.present(resource, row)),
|
|
30
|
+
meta: {
|
|
31
|
+
page: query.page,
|
|
32
|
+
perPage: query.perPage,
|
|
33
|
+
total,
|
|
34
|
+
lastPage: Math.max(1, Math.ceil(total / query.perPage)),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async show(actor, resource, id) {
|
|
39
|
+
const result = await sql `select * from (${this.source(actor, resource)}) as resources where id = ${id} limit 1`.execute(this.db);
|
|
40
|
+
return result.rows[0] ? this.present(resource, result.rows[0]) : undefined;
|
|
41
|
+
}
|
|
42
|
+
source(actor, resource) {
|
|
43
|
+
const global = actor.portal === "super-admin";
|
|
44
|
+
if (resource === "users")
|
|
45
|
+
return sql `select u.id,u.name,u.email,u.active,u.version from identity_users u
|
|
46
|
+
where exists (select 1 from identity_memberships m left join identity_custom_roles c on c.id=m.custom_role_id
|
|
47
|
+
where m.user_id=u.id and (m.custom_role_id is null or c.app_id=${actor.appId})
|
|
48
|
+
${global ? sql `` : sql `and m.tenant_id=${actor.tenant.id}`})
|
|
49
|
+
${global ? sql `or not exists(select 1 from identity_memberships m where m.user_id=u.id)` : sql ``}`;
|
|
50
|
+
if (resource === "organizations")
|
|
51
|
+
return sql `select id,name,active,version from identity_tenants ${global ? sql `` : sql `where id=${actor.tenant.id}`}`;
|
|
52
|
+
if (resource === "memberships")
|
|
53
|
+
return sql `select m.user_id||'~'||m.tenant_id||'~'||m.role_id as id,m.user_id as userId,m.tenant_id as tenantId,
|
|
54
|
+
coalesce(m.custom_role_id,m.role_id) as roleId,m.role_id as portal,m.active,m.version,u.name as userName,t.name as organizationName,
|
|
55
|
+
coalesce(c.name,m.role_id) as roleName from identity_memberships m
|
|
56
|
+
join identity_users u on u.id=m.user_id join identity_tenants t on t.id=m.tenant_id
|
|
57
|
+
left join identity_custom_roles c on c.id=m.custom_role_id
|
|
58
|
+
where (m.custom_role_id is null or c.app_id=${actor.appId}) ${global ? sql `` : sql `and m.tenant_id=${actor.tenant.id}`}`;
|
|
59
|
+
if (resource === "roles")
|
|
60
|
+
return sql `select r.id,r.id as name,r.id as portal,1 as system,null as tenantId,1 as active,r.version,(select json_group_array(permission_id) from identity_role_permissions p where p.role_id=r.id) as permissionIds from identity_roles r
|
|
61
|
+
union all select c.id,c.name,'user' as portal,0 as system,c.tenant_id as tenantId,c.active,c.version,
|
|
62
|
+
(select json_group_array(permission_id) from identity_custom_role_permissions p where p.role_id=c.id) as permissionIds
|
|
63
|
+
from identity_custom_roles c where c.app_id=${actor.appId} ${global ? sql `` : sql `and c.tenant_id=${actor.tenant.id}`}`;
|
|
64
|
+
if (resource === "permissions")
|
|
65
|
+
return sql `select p.id,coalesce(d.label,p.id) as label,
|
|
66
|
+
coalesce(d.owner,case when p.id like 'desk.%' then 'desk' else 'identity' end) as owner,
|
|
67
|
+
d.app_id as appId,
|
|
68
|
+
coalesce(d.portals,case when p.id='desk.user' then '["user"]' when p.id='desk.admin' then '["admin"]'
|
|
69
|
+
when p.id='desk.super-admin' then '["super-admin"]' when p.id='identity.manage' then '["admin","super-admin"]'
|
|
70
|
+
else '["user","admin","super-admin"]' end) as portals
|
|
71
|
+
from identity_permissions p left join identity_permission_declarations d on d.permission_id=p.id
|
|
72
|
+
where d.permission_id is null or d.app_id=${actor.appId}`;
|
|
73
|
+
if (resource === "sessions")
|
|
74
|
+
return sql `select token_hash as id,user_id as userId,tenant_id as tenantId,portal,expires_at as expiresAt from identity_sessions
|
|
75
|
+
where app_id=${actor.appId} and expires_at>${new Date().toISOString()}
|
|
76
|
+
${global ? sql `` : sql `and tenant_id=${actor.tenant.id}`}
|
|
77
|
+
${actor.portal === "user" ? sql `and user_id=${actor.user.id}` : sql ``}`;
|
|
78
|
+
return sql `select id,actor_id as actorId,tenant_id as tenantId,action,resource_id as resourceId,created_at as createdAt
|
|
79
|
+
from identity_audit_events where app_id=${actor.appId} ${global ? sql `` : sql `and tenant_id=${actor.tenant.id}`}`;
|
|
80
|
+
}
|
|
81
|
+
present(resource, row) {
|
|
82
|
+
if (resource === "permissions")
|
|
83
|
+
return { ...row, portals: JSON.parse(String(row.portals)) };
|
|
84
|
+
if (resource === "roles")
|
|
85
|
+
return {
|
|
86
|
+
...row,
|
|
87
|
+
version: Number(row.version),
|
|
88
|
+
active: Boolean(row.active),
|
|
89
|
+
system: Boolean(row.system),
|
|
90
|
+
permissionIds: JSON.parse(String(row.permissionIds)),
|
|
91
|
+
};
|
|
92
|
+
if (resource === "users" ||
|
|
93
|
+
resource === "organizations" ||
|
|
94
|
+
resource === "memberships")
|
|
95
|
+
return {
|
|
96
|
+
...row,
|
|
97
|
+
active: Boolean(row.active),
|
|
98
|
+
version: Number(row.version),
|
|
99
|
+
};
|
|
100
|
+
return row;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const resourceIdSchema: z.ZodString;
|
|
3
|
+
export declare const listSchema: z.ZodObject<{
|
|
4
|
+
page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
5
|
+
perPage: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
6
|
+
search: z.ZodDefault<z.ZodString>;
|
|
7
|
+
sort: z.ZodDefault<z.ZodEnum<{
|
|
8
|
+
email: "email";
|
|
9
|
+
id: "id";
|
|
10
|
+
name: "name";
|
|
11
|
+
}>>;
|
|
12
|
+
direction: z.ZodDefault<z.ZodEnum<{
|
|
13
|
+
asc: "asc";
|
|
14
|
+
desc: "desc";
|
|
15
|
+
}>>;
|
|
16
|
+
}, z.core.$strict>;
|
|
17
|
+
export declare const resourceListQuerySchema: z.ZodPipe<z.ZodObject<{
|
|
18
|
+
page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
19
|
+
search: z.ZodDefault<z.ZodString>;
|
|
20
|
+
sort: z.ZodDefault<z.ZodEnum<{
|
|
21
|
+
email: "email";
|
|
22
|
+
id: "id";
|
|
23
|
+
name: "name";
|
|
24
|
+
}>>;
|
|
25
|
+
direction: z.ZodDefault<z.ZodEnum<{
|
|
26
|
+
asc: "asc";
|
|
27
|
+
desc: "desc";
|
|
28
|
+
}>>;
|
|
29
|
+
per_page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
30
|
+
}, z.core.$strict>, z.ZodTransform<{
|
|
31
|
+
page: number;
|
|
32
|
+
search: string;
|
|
33
|
+
sort: "email" | "id" | "name";
|
|
34
|
+
direction: "asc" | "desc";
|
|
35
|
+
perPage: number;
|
|
36
|
+
}, {
|
|
37
|
+
page: number;
|
|
38
|
+
search: string;
|
|
39
|
+
sort: "email" | "id" | "name";
|
|
40
|
+
direction: "asc" | "desc";
|
|
41
|
+
per_page: number;
|
|
42
|
+
}>>;
|
|
43
|
+
export declare const membershipDeleteQuerySchema: z.ZodObject<{
|
|
44
|
+
expectedVersion: z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>;
|
|
45
|
+
}, z.core.$strict>;
|
|
46
|
+
export declare const userCreateSchema: z.ZodObject<{
|
|
47
|
+
name: z.ZodString;
|
|
48
|
+
email: z.ZodPipe<z.ZodEmail, z.ZodTransform<string, string>>;
|
|
49
|
+
password: z.ZodString;
|
|
50
|
+
roleId: z.ZodDefault<z.ZodString>;
|
|
51
|
+
tenantId: z.ZodOptional<z.ZodString>;
|
|
52
|
+
}, z.core.$strict>;
|
|
53
|
+
export declare const userUpdateSchema: z.ZodObject<{
|
|
54
|
+
name: z.ZodOptional<z.ZodString>;
|
|
55
|
+
email: z.ZodOptional<z.ZodPipe<z.ZodEmail, z.ZodTransform<string, string>>>;
|
|
56
|
+
active: z.ZodOptional<z.ZodBoolean>;
|
|
57
|
+
expectedVersion: z.ZodNumber;
|
|
58
|
+
}, z.core.$strict>;
|
|
59
|
+
export declare const organizationCreateSchema: z.ZodObject<{
|
|
60
|
+
id: z.ZodString;
|
|
61
|
+
name: z.ZodString;
|
|
62
|
+
active: z.ZodDefault<z.ZodBoolean>;
|
|
63
|
+
}, z.core.$strict>;
|
|
64
|
+
export declare const organizationUpdateSchema: z.ZodObject<{
|
|
65
|
+
name: z.ZodOptional<z.ZodString>;
|
|
66
|
+
active: z.ZodOptional<z.ZodBoolean>;
|
|
67
|
+
expectedVersion: z.ZodNumber;
|
|
68
|
+
}, z.core.$strict>;
|
|
69
|
+
export declare const membershipSchema: z.ZodObject<{
|
|
70
|
+
userId: z.ZodString;
|
|
71
|
+
tenantId: z.ZodOptional<z.ZodString>;
|
|
72
|
+
roleId: z.ZodString;
|
|
73
|
+
}, z.core.$strict>;
|
|
74
|
+
export declare const roleUpdateSchema: z.ZodObject<{
|
|
75
|
+
permissionIds: z.ZodArray<z.ZodString>;
|
|
76
|
+
expectedVersion: z.ZodNumber;
|
|
77
|
+
}, z.core.$strict>;
|
|
78
|
+
export declare const profileSchema: z.ZodObject<{
|
|
79
|
+
name: z.ZodString;
|
|
80
|
+
expectedVersion: z.ZodNumber;
|
|
81
|
+
}, z.core.$strict>;
|
|
82
|
+
export declare const customRoleCreateSchema: z.ZodObject<{
|
|
83
|
+
name: z.ZodString;
|
|
84
|
+
permissionIds: z.ZodArray<z.ZodString>;
|
|
85
|
+
tenantId: z.ZodOptional<z.ZodString>;
|
|
86
|
+
}, z.core.$strict>;
|
|
87
|
+
export declare const customRoleUpdateSchema: z.ZodObject<{
|
|
88
|
+
name: z.ZodOptional<z.ZodString>;
|
|
89
|
+
permissionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
90
|
+
active: z.ZodOptional<z.ZodBoolean>;
|
|
91
|
+
expectedVersion: z.ZodNumber;
|
|
92
|
+
}, z.core.$strict>;
|
|
93
|
+
export declare const membershipUpdateSchema: z.ZodObject<{
|
|
94
|
+
roleId: z.ZodString;
|
|
95
|
+
active: z.ZodBoolean;
|
|
96
|
+
expectedVersion: z.ZodNumber;
|
|
97
|
+
}, z.core.$strict>;
|
|
98
|
+
export declare const settingsSchema: z.ZodObject<{
|
|
99
|
+
displayName: z.ZodString;
|
|
100
|
+
expectedVersion: z.ZodNumber;
|
|
101
|
+
locale: z.ZodEnum<{
|
|
102
|
+
en: "en";
|
|
103
|
+
"en-GB": "en-GB";
|
|
104
|
+
"en-US": "en-US";
|
|
105
|
+
}>;
|
|
106
|
+
timeZone: z.ZodString;
|
|
107
|
+
}, z.core.$strict>;
|
|
108
|
+
export declare const securitySettingsSchema: z.ZodObject<{
|
|
109
|
+
sessionSeconds: z.ZodNumber;
|
|
110
|
+
expectedVersion: z.ZodNumber;
|
|
111
|
+
}, z.core.$strict>;
|
|
112
|
+
export type IdentityListQuery = z.infer<typeof listSchema>;
|
|
113
|
+
export type IdentityResource = "users" | "organizations" | "memberships" | "roles" | "permissions" | "sessions" | "audit-events";
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { identityKeySchema } from "./identity.schema.js";
|
|
3
|
+
export const resourceIdSchema = z
|
|
4
|
+
.string()
|
|
5
|
+
.regex(/^[a-zA-Z0-9._~-]{1,300}$/)
|
|
6
|
+
.refine((value) => !/^\.+$/.test(value), "Use an identifier with a letter or number.");
|
|
7
|
+
export const listSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
page: z.coerce.number().int().min(1).max(100000).default(1),
|
|
10
|
+
perPage: z.coerce.number().int().min(1).max(100).default(20),
|
|
11
|
+
search: z.string().trim().max(100).default(""),
|
|
12
|
+
sort: z.enum(["id", "name", "email"]).default("id"),
|
|
13
|
+
direction: z.enum(["asc", "desc"]).default("asc"),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
export const resourceListQuerySchema = listSchema
|
|
17
|
+
.omit({ perPage: true })
|
|
18
|
+
.extend({ per_page: listSchema.shape.perPage })
|
|
19
|
+
.strict()
|
|
20
|
+
.transform(({ per_page, ...input }) => ({ ...input, perPage: per_page }));
|
|
21
|
+
export const membershipDeleteQuerySchema = z
|
|
22
|
+
.object({
|
|
23
|
+
expectedVersion: z
|
|
24
|
+
.string()
|
|
25
|
+
.regex(/^(0|[1-9][0-9]{0,9})$/)
|
|
26
|
+
.transform(Number),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
const name = z.string().trim().min(1).max(100);
|
|
30
|
+
const email = z
|
|
31
|
+
.email()
|
|
32
|
+
.max(254)
|
|
33
|
+
.transform((v) => v.trim().toLowerCase());
|
|
34
|
+
export const userCreateSchema = z
|
|
35
|
+
.object({
|
|
36
|
+
name,
|
|
37
|
+
email,
|
|
38
|
+
password: z.string().min(12).max(256),
|
|
39
|
+
roleId: resourceIdSchema.default("user"),
|
|
40
|
+
tenantId: identityKeySchema.optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
const expectedVersion = z.number().int().min(0);
|
|
44
|
+
export const userUpdateSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
name: name.optional(),
|
|
47
|
+
email: email.optional(),
|
|
48
|
+
active: z.boolean().optional(),
|
|
49
|
+
expectedVersion,
|
|
50
|
+
})
|
|
51
|
+
.strict()
|
|
52
|
+
.refine((v) => Object.keys(v).length > 1, "Provide a changed field.");
|
|
53
|
+
export const organizationCreateSchema = z
|
|
54
|
+
.object({ id: identityKeySchema, name, active: z.boolean().default(true) })
|
|
55
|
+
.strict();
|
|
56
|
+
export const organizationUpdateSchema = z
|
|
57
|
+
.object({
|
|
58
|
+
name: name.optional(),
|
|
59
|
+
active: z.boolean().optional(),
|
|
60
|
+
expectedVersion,
|
|
61
|
+
})
|
|
62
|
+
.strict()
|
|
63
|
+
.refine((v) => Object.keys(v).length > 1, "Provide a changed field.");
|
|
64
|
+
export const membershipSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
userId: identityKeySchema,
|
|
67
|
+
tenantId: identityKeySchema.optional(),
|
|
68
|
+
roleId: resourceIdSchema,
|
|
69
|
+
})
|
|
70
|
+
.strict();
|
|
71
|
+
export const roleUpdateSchema = z
|
|
72
|
+
.object({
|
|
73
|
+
permissionIds: z.array(resourceIdSchema).max(100),
|
|
74
|
+
expectedVersion,
|
|
75
|
+
})
|
|
76
|
+
.strict();
|
|
77
|
+
export const profileSchema = z.object({ name, expectedVersion }).strict();
|
|
78
|
+
export const customRoleCreateSchema = z
|
|
79
|
+
.object({
|
|
80
|
+
name,
|
|
81
|
+
permissionIds: z.array(resourceIdSchema).max(100),
|
|
82
|
+
tenantId: identityKeySchema.optional(),
|
|
83
|
+
})
|
|
84
|
+
.strict();
|
|
85
|
+
export const customRoleUpdateSchema = z
|
|
86
|
+
.object({
|
|
87
|
+
name: name.optional(),
|
|
88
|
+
permissionIds: z.array(resourceIdSchema).max(100).optional(),
|
|
89
|
+
active: z.boolean().optional(),
|
|
90
|
+
expectedVersion,
|
|
91
|
+
})
|
|
92
|
+
.strict()
|
|
93
|
+
.refine((input) => Object.keys(input).length > 1, "Provide a changed field.");
|
|
94
|
+
export const membershipUpdateSchema = z
|
|
95
|
+
.object({ roleId: resourceIdSchema, active: z.boolean(), expectedVersion })
|
|
96
|
+
.strict();
|
|
97
|
+
export const settingsSchema = z
|
|
98
|
+
.object({
|
|
99
|
+
displayName: name,
|
|
100
|
+
expectedVersion,
|
|
101
|
+
locale: z.enum(["en", "en-US", "en-GB"]),
|
|
102
|
+
timeZone: z
|
|
103
|
+
.string()
|
|
104
|
+
.max(100)
|
|
105
|
+
.refine((v) => {
|
|
106
|
+
try {
|
|
107
|
+
new Intl.DateTimeFormat("en", { timeZone: v });
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}, "Use a supported IANA time zone."),
|
|
114
|
+
})
|
|
115
|
+
.strict();
|
|
116
|
+
export const securitySettingsSchema = z
|
|
117
|
+
.object({
|
|
118
|
+
sessionSeconds: z.number().int().min(300).max(86400),
|
|
119
|
+
expectedVersion,
|
|
120
|
+
})
|
|
121
|
+
.strict();
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type Kysely } from "kysely";
|
|
2
|
+
import type { IdentitySchema, Principal } from "./identity.types.js";
|
|
3
|
+
import { type IdentityListQuery, type IdentityResource } from "./identity.administration-schema.js";
|
|
4
|
+
export declare class IdentityAdministrationService {
|
|
5
|
+
private readonly db;
|
|
6
|
+
private readonly appName;
|
|
7
|
+
private readonly repository;
|
|
8
|
+
private readonly roles;
|
|
9
|
+
private readonly presentationService;
|
|
10
|
+
constructor(db: Kysely<IdentitySchema>, appName: string);
|
|
11
|
+
list(actor: Principal, resource: IdentityResource, query: IdentityListQuery): Promise<{
|
|
12
|
+
data: Record<string, unknown>[];
|
|
13
|
+
meta: {
|
|
14
|
+
page: number;
|
|
15
|
+
perPage: number;
|
|
16
|
+
total: number;
|
|
17
|
+
lastPage: number;
|
|
18
|
+
};
|
|
19
|
+
}>;
|
|
20
|
+
show(actor: Principal, resource: IdentityResource, id: string): Promise<Record<string, unknown>>;
|
|
21
|
+
create(actor: Principal, resource: IdentityResource, raw: unknown): Promise<{
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
active: boolean;
|
|
25
|
+
version: number;
|
|
26
|
+
} | {
|
|
27
|
+
id: string;
|
|
28
|
+
userId: string;
|
|
29
|
+
tenantId: string;
|
|
30
|
+
roleId: string;
|
|
31
|
+
portal: import("./identity.types.js").Portal;
|
|
32
|
+
active: boolean;
|
|
33
|
+
version: number;
|
|
34
|
+
}>;
|
|
35
|
+
update(actor: Principal, resource: IdentityResource, id: string, raw: unknown): Promise<{
|
|
36
|
+
id: string;
|
|
37
|
+
userId: string;
|
|
38
|
+
tenantId: string;
|
|
39
|
+
roleId: string;
|
|
40
|
+
portal: import("./identity.types.js").Portal;
|
|
41
|
+
active: boolean;
|
|
42
|
+
version: number;
|
|
43
|
+
} | {
|
|
44
|
+
active: boolean;
|
|
45
|
+
version: number;
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
} | {
|
|
49
|
+
id: string;
|
|
50
|
+
permissionIds: string[];
|
|
51
|
+
version: number;
|
|
52
|
+
}>;
|
|
53
|
+
remove(actor: Principal, resource: IdentityResource, id: string, expectedVersion?: number): Promise<void>;
|
|
54
|
+
profile(actor: Principal, raw?: unknown): Promise<{
|
|
55
|
+
active: boolean;
|
|
56
|
+
version: number;
|
|
57
|
+
email: string;
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
}>;
|
|
61
|
+
settings(actor: Principal, raw?: unknown): Promise<{
|
|
62
|
+
displayName: string;
|
|
63
|
+
locale: string;
|
|
64
|
+
timeZone: string;
|
|
65
|
+
version: number;
|
|
66
|
+
}>;
|
|
67
|
+
applicationSettings(actor: Principal, security: boolean, raw: unknown, fallbackSeconds: number): Promise<{
|
|
68
|
+
sessionSeconds: number;
|
|
69
|
+
version: number;
|
|
70
|
+
displayName?: undefined;
|
|
71
|
+
locale?: undefined;
|
|
72
|
+
timeZone?: undefined;
|
|
73
|
+
} | {
|
|
74
|
+
sessionSeconds?: undefined;
|
|
75
|
+
displayName: string;
|
|
76
|
+
locale: string;
|
|
77
|
+
timeZone: string;
|
|
78
|
+
version: number;
|
|
79
|
+
}>;
|
|
80
|
+
private revision;
|
|
81
|
+
presentation(actor: Principal): Promise<{
|
|
82
|
+
displayName: string;
|
|
83
|
+
organizationDisplayName: string;
|
|
84
|
+
locale: string;
|
|
85
|
+
timeZone: string;
|
|
86
|
+
}>;
|
|
87
|
+
configuration(appId: string, requiresOrganizationId: boolean): Promise<{
|
|
88
|
+
displayName: string;
|
|
89
|
+
requiresOrganizationId: boolean;
|
|
90
|
+
}>;
|
|
91
|
+
private allow;
|
|
92
|
+
private tenant;
|
|
93
|
+
private assignableRole;
|
|
94
|
+
private activeTenant;
|
|
95
|
+
private unprivilegedUser;
|
|
96
|
+
private mutate;
|
|
97
|
+
}
|