@open-core/identity 1.2.0 → 1.2.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.
@@ -0,0 +1,164 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { injectable, inject } from "tsyringe";
14
+ import { Server } from "@open-core/framework";
15
+ import { IDENTITY_OPTIONS } from "../../tokens";
16
+ import { IdentityStore, RoleStore } from "../../contracts";
17
+ /**
18
+ * Authorization provider implementation for the OpenCore Framework.
19
+ *
20
+ * This provider resolves player principals (roles and permissions) by
21
+ * interacting with the configured {@link IdentityStore} and {@link RoleStore}.
22
+ * It includes a high-performance in-memory cache to minimize database
23
+ * overhead during frequent security checks (e.g., in `@Guard` decorators).
24
+ *
25
+ * @injectable
26
+ * @public
27
+ */
28
+ let IdentityPrincipalProvider = class IdentityPrincipalProvider extends Server.PrincipalProviderContract {
29
+ /**
30
+ * Initializes a new instance of the IdentityPrincipalProvider.
31
+ *
32
+ * @param options - Identity system configuration options.
33
+ * @param accountStore - Persistence layer for account data.
34
+ * @param roleStore - Optional persistence layer for dynamic roles.
35
+ */
36
+ constructor(options, accountStore, roleStore) {
37
+ super();
38
+ this.options = options;
39
+ this.accountStore = accountStore;
40
+ this.roleStore = roleStore;
41
+ /**
42
+ * In-memory cache for resolved principals.
43
+ * Key: clientId (number)
44
+ */
45
+ this.cache = new Map();
46
+ this.cacheTtl = options.principal.cacheTtl ?? 300000; // 5 minutes default
47
+ }
48
+ /**
49
+ * Resolves the security Principal for a connected player.
50
+ *
51
+ * This method first checks the internal cache. If missing or expired,
52
+ * it resolves the account and its effective permissions.
53
+ *
54
+ * @param player - The framework player entity.
55
+ * @returns A promise resolving to the {@link Server.Principal} or null if not authenticated.
56
+ */
57
+ async getPrincipal(player) {
58
+ const clientId = player.clientID;
59
+ const cached = this.cache.get(clientId);
60
+ if (cached && cached.expiresAt > Date.now()) {
61
+ return cached.principal;
62
+ }
63
+ const linkedId = player.accountID;
64
+ if (!linkedId)
65
+ return null;
66
+ const principal = await this.resolvePrincipal(linkedId);
67
+ if (principal) {
68
+ this.cache.set(clientId, {
69
+ principal,
70
+ expiresAt: Date.now() + this.cacheTtl,
71
+ });
72
+ }
73
+ return principal;
74
+ }
75
+ /**
76
+ * Invalidates the cache and re-resolves the principal for a player.
77
+ *
78
+ * @param player - The player whose principal should be refreshed.
79
+ */
80
+ async refreshPrincipal(player) {
81
+ this.cache.delete(player.clientID);
82
+ await this.getPrincipal(player);
83
+ }
84
+ /**
85
+ * Resolves a principal for offline workflows using a stable account ID.
86
+ *
87
+ * @param linkedID - The linked account identifier.
88
+ * @returns A promise resolving to the principal or null.
89
+ */
90
+ async getPrincipalByLinkedID(linkedID) {
91
+ return this.resolvePrincipal(linkedID);
92
+ }
93
+ /**
94
+ * Internal logic to resolve effective permissions and construct the Principal.
95
+ *
96
+ * @param linkedId - The stable account ID.
97
+ * @returns Resolves the role, merges permissions, and returns the Principal.
98
+ * @internal
99
+ */
100
+ async resolvePrincipal(linkedId) {
101
+ const account = await this.accountStore.findByLinkedId(linkedId);
102
+ if (!account)
103
+ return null;
104
+ let role;
105
+ if (this.options.principal.mode === "roles") {
106
+ role = this.options.principal.roles?.[account.roleName];
107
+ }
108
+ else if (this.roleStore) {
109
+ const dbRole = await this.roleStore.findByName(account.roleName);
110
+ if (dbRole)
111
+ role = dbRole;
112
+ }
113
+ if (!role) {
114
+ const defaultName = this.options.principal.defaultRole || "user";
115
+ role = this.options.principal.roles?.[defaultName];
116
+ }
117
+ if (!role)
118
+ return null;
119
+ const effectivePermissions = this.mergePermissions(role.permissions, account.customPermissions);
120
+ return {
121
+ id: account.linkedId,
122
+ name: role.displayName || role.name,
123
+ rank: role.rank,
124
+ permissions: effectivePermissions,
125
+ meta: {
126
+ accountId: account.id,
127
+ roleName: role.name,
128
+ },
129
+ };
130
+ }
131
+ /**
132
+ * Merges role-based permissions with account-specific overrides.
133
+ *
134
+ * Overrides starting with '-' are removed, and those starting with '+'
135
+ * (or without prefix) are added to the final set.
136
+ *
137
+ * @param base - Base permissions from the role.
138
+ * @param overrides - Custom overrides from the account.
139
+ * @returns The unified list of effective permissions.
140
+ * @internal
141
+ */
142
+ mergePermissions(base, overrides) {
143
+ const perms = new Set(base);
144
+ for (const override of overrides) {
145
+ if (override.startsWith("-")) {
146
+ perms.delete(override.substring(1));
147
+ }
148
+ else if (override.startsWith("+")) {
149
+ perms.add(override.substring(1));
150
+ }
151
+ else {
152
+ perms.add(override);
153
+ }
154
+ }
155
+ return Array.from(perms);
156
+ }
157
+ };
158
+ IdentityPrincipalProvider = __decorate([
159
+ injectable(),
160
+ __param(0, inject(IDENTITY_OPTIONS)),
161
+ __metadata("design:paramtypes", [Object, IdentityStore,
162
+ RoleStore])
163
+ ], IdentityPrincipalProvider);
164
+ export { IdentityPrincipalProvider };
@@ -1,78 +1,73 @@
1
- import { Server } from "@open-core/framework";
2
- import type { Account } from "../entities/account.entity";
3
- import type { AccountIdentifiers, BanOptions, IdentifierType } from "../types";
4
- import { AccountRepository } from "../repositories/account.repository";
5
- import { RoleService } from "./role.service";
1
+ import { IdentityStore } from "../contracts";
2
+ import type { IdentityAccount, IdentityOptions } from "../types";
3
+ /**
4
+ * High-level service for managing identity accounts and security policies.
5
+ *
6
+ * Provides a programmer-friendly API for account administration, including
7
+ * role assignment, permission overrides, and ban management.
8
+ *
9
+ * @public
10
+ * @injectable
11
+ */
6
12
  export declare class AccountService {
7
- private readonly repo;
8
- private readonly roleService;
9
- private readonly config;
10
- constructor(repo: AccountRepository, roleService: RoleService, config: Server.ConfigService);
11
- findById(id: number): Promise<Account | null>;
12
- findByLinkedId(linkedId: string): Promise<Account | null>;
13
- findByIdentifier(type: IdentifierType, value: string): Promise<Account | null>;
14
- findOrCreate(identifiers: AccountIdentifiers): Promise<{
15
- account: Account;
16
- isNew: boolean;
17
- }>;
18
- ban(accountId: number, options: BanOptions): Promise<void>;
19
- unban(accountId: number): Promise<void>;
20
- isBanExpired(account: Account): boolean;
13
+ private readonly store;
14
+ private readonly options;
15
+ constructor(store: IdentityStore, options: IdentityOptions);
21
16
  /**
22
- * Add a custom permission to an account (override/additional to role).
17
+ * Retrieves an account by its unique numeric or internal ID.
23
18
  *
24
- * @param accountId - Account ID
25
- * @param permission - Permission string to add
19
+ * @param id - The internal account identifier.
20
+ * @returns A promise resolving to the account or null if not found.
26
21
  */
27
- addCustomPermission(accountId: number, permission: string): Promise<void>;
22
+ findById(id: string): Promise<IdentityAccount | null>;
28
23
  /**
29
- * Remove a custom permission from an account.
24
+ * Retrieves an account by its stable linked ID.
30
25
  *
31
- * @param accountId - Account ID
32
- * @param permission - Permission string to remove
26
+ * @param linkedId - The stable ID (UUID or external system ID).
27
+ * @returns A promise resolving to the account or null if not found.
33
28
  */
34
- removeCustomPermission(accountId: number, permission: string): Promise<void>;
29
+ findByLinkedId(linkedId: string): Promise<IdentityAccount | null>;
35
30
  /**
36
- * Get effective permissions for an account (role + custom).
31
+ * Assigns a security role to an account.
37
32
  *
38
- * @param accountId - Account ID
39
- * @returns Combined permissions array
33
+ * @param accountId - The linked ID of the account.
34
+ * @param roleName - Technical name of the role to assign.
40
35
  */
41
- getEffectivePermissions(accountId: number): Promise<string[]>;
36
+ assignRole(accountId: string, roleName: string): Promise<void>;
42
37
  /**
43
- * Assign a role to an account.
38
+ * Grants a custom permission override to an account.
44
39
  *
45
- * @param accountId - Account ID
46
- * @param roleId - Role ID to assign (null to remove role)
47
- */
48
- assignRole(accountId: number, roleId: number | null): Promise<void>;
49
- /**
50
- * Combine role permissions with account custom permissions.
51
- * Custom permissions starting with '-' negate the base permission.
40
+ * This override takes precedence over role permissions.
41
+ * Use the `+` prefix for clarity (optional).
52
42
  *
53
- * @param role - Role with base permissions
54
- * @param customPerms - Account custom permissions
55
- * @returns Combined permissions array
43
+ * @param accountId - The linked ID of the account.
44
+ * @param permission - The permission string to grant.
56
45
  */
57
- private combinePermissions;
58
- touchLastLogin(accountId: number, date?: Date): Promise<void>;
59
- private lookupExisting;
60
- private identifierPriority;
46
+ addCustomPermission(accountId: string, permission: string): Promise<void>;
61
47
  /**
62
- * Find account by username (for credentials auth)
63
- * Note: This is a basic implementation. For production, add an index on username.
48
+ * Revokes a custom permission override.
49
+ *
50
+ * To explicitly deny a permission that a role might grant, use the `-` prefix
51
+ * (e.g., `-chat.use`).
52
+ *
53
+ * @param accountId - The linked ID of the account.
54
+ * @param permission - The permission string to remove or revoke.
64
55
  */
65
- findByUsername(_username: string): Promise<Account | null>;
56
+ removeCustomPermission(accountId: string, permission: string): Promise<void>;
66
57
  /**
67
- * Update account identifiers (for credentials auth identifier merging)
58
+ * Prohibits an account from connecting to the server.
59
+ *
60
+ * @param accountId - The linked ID of the account.
61
+ * @param options - Ban details including optional reason and duration.
68
62
  */
69
- updateIdentifiers(): Promise<void>;
63
+ ban(accountId: string, options?: {
64
+ reason?: string;
65
+ durationMs?: number;
66
+ }): Promise<void>;
70
67
  /**
71
- * Create account with credentials (for CredentialsAuthProvider)
68
+ * Lifts an active ban from an account.
69
+ *
70
+ * @param accountId - The linked ID of the account.
72
71
  */
73
- createWithCredentials(input: {
74
- username: string;
75
- passwordHash: string;
76
- identifiers: AccountIdentifiers;
77
- }): Promise<Account>;
72
+ unban(accountId: string): Promise<void>;
78
73
  }
@@ -7,198 +7,115 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
- import { injectable } from "tsyringe";
11
- import { Server } from "@open-core/framework";
12
- import { randomUUID } from "crypto";
13
- import { AccountRepository } from "../repositories/account.repository";
14
- import { RoleService } from "./role.service";
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { injectable, inject } from "tsyringe";
14
+ import { IDENTITY_OPTIONS } from "../tokens";
15
+ import { IdentityStore } from "../contracts";
16
+ /**
17
+ * High-level service for managing identity accounts and security policies.
18
+ *
19
+ * Provides a programmer-friendly API for account administration, including
20
+ * role assignment, permission overrides, and ban management.
21
+ *
22
+ * @public
23
+ * @injectable
24
+ */
15
25
  let AccountService = class AccountService {
16
- constructor(repo, roleService, config) {
17
- this.repo = repo;
18
- this.roleService = roleService;
19
- this.config = config;
26
+ constructor(store, options) {
27
+ this.store = store;
28
+ this.options = options;
20
29
  }
30
+ /**
31
+ * Retrieves an account by its unique numeric or internal ID.
32
+ *
33
+ * @param id - The internal account identifier.
34
+ * @returns A promise resolving to the account or null if not found.
35
+ */
21
36
  async findById(id) {
22
- return this.repo.findById(id);
37
+ return this.store.findByLinkedId(id); // Using linkedId as the primary public handle
23
38
  }
39
+ /**
40
+ * Retrieves an account by its stable linked ID.
41
+ *
42
+ * @param linkedId - The stable ID (UUID or external system ID).
43
+ * @returns A promise resolving to the account or null if not found.
44
+ */
24
45
  async findByLinkedId(linkedId) {
25
- return this.repo.findByLinkedId(linkedId);
26
- }
27
- async findByIdentifier(type, value) {
28
- return this.repo.findByIdentifier(type, value);
46
+ return this.store.findByLinkedId(linkedId);
29
47
  }
30
- async findOrCreate(identifiers) {
31
- const existing = await this.lookupExisting(identifiers);
32
- if (existing) {
33
- return { account: existing, isNew: false };
34
- }
35
- // Get default role for new accounts
36
- const defaultRole = await this.roleService.getDefaultRole();
37
- // Auto-generate linkedId by default (UUID format for local accounts)
38
- const created = await this.repo.createAccount({
39
- linkedId: randomUUID(),
40
- externalSource: "local",
41
- license: identifiers.license ?? null,
42
- discord: identifiers.discord ?? null,
43
- steam: identifiers.steam ?? null,
44
- username: null,
45
- roleId: defaultRole?.id ?? null,
46
- });
47
- return { account: created, isNew: true };
48
- }
49
- async ban(accountId, options) {
50
- const expires = options.durationMs
51
- ? new Date(Date.now() + options.durationMs)
52
- : null;
53
- await this.repo.setBan(accountId, true, options.reason ?? "Banned", expires);
54
- }
55
- async unban(accountId) {
56
- await this.repo.setBan(accountId, false, null, null);
57
- }
58
- isBanExpired(account) {
59
- if (!account.banned)
60
- return false;
61
- if (!account.banExpires)
62
- return false;
63
- return account.banExpires.getTime() <= Date.now();
48
+ /**
49
+ * Assigns a security role to an account.
50
+ *
51
+ * @param accountId - The linked ID of the account.
52
+ * @param roleName - Technical name of the role to assign.
53
+ */
54
+ async assignRole(accountId, roleName) {
55
+ await this.store.update(accountId, { roleName });
64
56
  }
65
57
  /**
66
- * Add a custom permission to an account (override/additional to role).
58
+ * Grants a custom permission override to an account.
59
+ *
60
+ * This override takes precedence over role permissions.
61
+ * Use the `+` prefix for clarity (optional).
67
62
  *
68
- * @param accountId - Account ID
69
- * @param permission - Permission string to add
63
+ * @param accountId - The linked ID of the account.
64
+ * @param permission - The permission string to grant.
70
65
  */
71
66
  async addCustomPermission(accountId, permission) {
72
- const account = await this.repo.findById(accountId);
67
+ const account = await this.store.findByLinkedId(accountId);
73
68
  if (!account)
74
69
  return;
75
- const permissions = new Set(account.customPermissions ?? []);
70
+ const permissions = new Set(account.customPermissions);
76
71
  permissions.add(permission);
77
- await this.repo.updateCustomPermissions(accountId, Array.from(permissions));
72
+ await this.store.update(accountId, {
73
+ customPermissions: Array.from(permissions),
74
+ });
78
75
  }
79
76
  /**
80
- * Remove a custom permission from an account.
77
+ * Revokes a custom permission override.
78
+ *
79
+ * To explicitly deny a permission that a role might grant, use the `-` prefix
80
+ * (e.g., `-chat.use`).
81
81
  *
82
- * @param accountId - Account ID
83
- * @param permission - Permission string to remove
82
+ * @param accountId - The linked ID of the account.
83
+ * @param permission - The permission string to remove or revoke.
84
84
  */
85
85
  async removeCustomPermission(accountId, permission) {
86
- const account = await this.repo.findById(accountId);
86
+ const account = await this.store.findByLinkedId(accountId);
87
87
  if (!account)
88
88
  return;
89
- const filtered = (account.customPermissions ?? []).filter((p) => p !== permission && p !== `-${permission}`);
90
- await this.repo.updateCustomPermissions(accountId, filtered);
91
- }
92
- /**
93
- * Get effective permissions for an account (role + custom).
94
- *
95
- * @param accountId - Account ID
96
- * @returns Combined permissions array
97
- */
98
- async getEffectivePermissions(accountId) {
99
- const result = await this.repo.findByIdWithRole(accountId);
100
- if (!result)
101
- return [];
102
- return this.combinePermissions(result.role, result.account.customPermissions);
89
+ const permissions = new Set(account.customPermissions);
90
+ permissions.delete(permission);
91
+ await this.store.update(accountId, {
92
+ customPermissions: Array.from(permissions),
93
+ });
103
94
  }
104
95
  /**
105
- * Assign a role to an account.
96
+ * Prohibits an account from connecting to the server.
106
97
  *
107
- * @param accountId - Account ID
108
- * @param roleId - Role ID to assign (null to remove role)
98
+ * @param accountId - The linked ID of the account.
99
+ * @param options - Ban details including optional reason and duration.
109
100
  */
110
- async assignRole(accountId, roleId) {
111
- await this.repo.updateRole(accountId, roleId);
101
+ async ban(accountId, options = {}) {
102
+ const expiresAt = options.durationMs
103
+ ? new Date(Date.now() + options.durationMs)
104
+ : null;
105
+ await this.store.setBan(accountId, true, options.reason, expiresAt);
112
106
  }
113
107
  /**
114
- * Combine role permissions with account custom permissions.
115
- * Custom permissions starting with '-' negate the base permission.
108
+ * Lifts an active ban from an account.
116
109
  *
117
- * @param role - Role with base permissions
118
- * @param customPerms - Account custom permissions
119
- * @returns Combined permissions array
110
+ * @param accountId - The linked ID of the account.
120
111
  */
121
- combinePermissions(role, customPerms) {
122
- const base = new Set(role?.permissions ?? []);
123
- for (const perm of customPerms) {
124
- if (perm.startsWith("-")) {
125
- // Negation: remove the base permission
126
- base.delete(perm.slice(1));
127
- }
128
- else {
129
- // Addition: add custom permission
130
- base.add(perm);
131
- }
132
- }
133
- return Array.from(base);
134
- }
135
- async touchLastLogin(accountId, date = new Date()) {
136
- await this.repo.updateLastLogin(accountId, date);
137
- }
138
- async lookupExisting(identifiers) {
139
- const ordered = this.identifierPriority();
140
- for (const key of ordered) {
141
- const value = identifiers[key];
142
- if (!value)
143
- continue;
144
- const found = await this.repo.findByIdentifier(key, value);
145
- if (found)
146
- return found;
147
- }
148
- // Fallback: try any provided identifiers
149
- for (const [key, value] of Object.entries(identifiers)) {
150
- if (!value)
151
- continue;
152
- const found = await this.repo.findByIdentifier(key, value);
153
- if (found)
154
- return found;
155
- }
156
- return null;
157
- }
158
- identifierPriority() {
159
- const fromConfig = this.config.get("identity_primary_identifier", "license");
160
- const base = ["license", "discord", "steam"];
161
- return [fromConfig, ...base.filter((id) => id !== fromConfig)];
162
- }
163
- /**
164
- * Find account by username (for credentials auth)
165
- * Note: This is a basic implementation. For production, add an index on username.
166
- */
167
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
168
- async findByUsername(_username) {
169
- // TODO: Add findByUsername method to AccountRepository for better performance
170
- // For now, this is a placeholder that credentials auth will need
171
- throw new Error("findByUsername not implemented - add to AccountRepository for credentials auth");
172
- }
173
- /**
174
- * Update account identifiers (for credentials auth identifier merging)
175
- */
176
- async updateIdentifiers() {
177
- // This is a placeholder - in production you'd want a proper update method
178
- // For now, credentials auth can work without this
179
- console.warn("updateIdentifiers not fully implemented - identifiers not merged");
180
- }
181
- /**
182
- * Create account with credentials (for CredentialsAuthProvider)
183
- */
184
- async createWithCredentials(input) {
185
- const defaultRole = await this.roleService.getDefaultRole();
186
- // Note: This creates an account without password_hash field
187
- // You'll need to add password_hash to Account entity and migration 005
188
- return this.repo.createAccount({
189
- linkedId: randomUUID(),
190
- externalSource: "credentials",
191
- username: input.username,
192
- license: input.identifiers.license ?? null,
193
- discord: input.identifiers.discord ?? null,
194
- steam: input.identifiers.steam ?? null,
195
- roleId: defaultRole?.id ?? null,
196
- });
112
+ async unban(accountId) {
113
+ await this.store.setBan(accountId, false);
197
114
  }
198
115
  };
199
116
  AccountService = __decorate([
200
117
  injectable(),
201
- __metadata("design:paramtypes", [AccountRepository,
202
- RoleService, Server.ConfigService])
118
+ __param(1, inject(IDENTITY_OPTIONS)),
119
+ __metadata("design:paramtypes", [IdentityStore, Object])
203
120
  ], AccountService);
204
121
  export { AccountService };