@manablox/auth 0.2.0 → 0.4.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/dist/index.d.ts +344 -0
- package/dist/index.js +585 -0
- package/package.json +14 -7
- package/src/api-key.ts +0 -164
- package/src/index.ts +0 -164
- package/src/password.ts +0 -17
- package/src/rbac.ts +0 -134
- package/src/user.service.ts +0 -192
- package/test/api-key.test.ts +0 -18
- package/test/rbac.test.ts +0 -128
- package/tsconfig.json +0 -1
- package/vitest.config.ts +0 -2
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { ALL_PERMISSIONS, AuditActor, AuthConfig, BUILT_IN_ROLES, BuiltInRole, CONTENT_ACTIONS, ContentAction, ContentPermission, ContentPermission as ContentPermission$1, Grant, Manablox, PERMISSION_GROUPS, Permission, Permission as Permission$1, PermissionGroup, SpaceRole, SpaceRole as SpaceRole$1, grantsCover, intersectGrants, isBuiltInRole, normaliseGrants, parseGrant, permissionsFor, typesCoveredBy } from "@manablox/core";
|
|
2
|
+
import { Database, MembershipRow, Repositories, SpaceRow } from "@manablox/db";
|
|
3
|
+
//#region src/rbac.d.ts
|
|
4
|
+
export interface Principal {
|
|
5
|
+
userId: string;
|
|
6
|
+
email: string;
|
|
7
|
+
/** Instance-wide role; `superadmin` short-circuits every space check. */
|
|
8
|
+
role: string;
|
|
9
|
+
/** Space id → the name of the role held there. */
|
|
10
|
+
spaces: Record<string, SpaceRole$1>;
|
|
11
|
+
/**
|
|
12
|
+
* Space id → the grants that role carries, resolved when the principal is. Absent for
|
|
13
|
+
* a space whose role is built in: those are answered from the table above.
|
|
14
|
+
*/
|
|
15
|
+
permissions?: Record<string, readonly string[]>;
|
|
16
|
+
/** True when the request authenticated with an API key rather than a session. */
|
|
17
|
+
viaApiKey?: boolean;
|
|
18
|
+
/** The key that authenticated the request, for the audit log. */
|
|
19
|
+
apiKeyId?: string | null;
|
|
20
|
+
apiKeyName?: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Spaces this principal is confined to, or `null`/absent for no confinement. Set by an
|
|
23
|
+
* API key that was issued with a space restriction: it narrows the key below its
|
|
24
|
+
* owner's own access and, unlike a role, it also binds a superadmin.
|
|
25
|
+
*/
|
|
26
|
+
allowedSpaceIds?: string[] | null;
|
|
27
|
+
/**
|
|
28
|
+
* Grants this principal is confined to, or `null`/absent for no confinement. Set by an
|
|
29
|
+
* API key issued with a permission restriction: like `allowedSpaceIds` it only ever
|
|
30
|
+
* narrows the owner's own access, and it binds a superadmin too.
|
|
31
|
+
*/
|
|
32
|
+
allowedGrants?: readonly string[] | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The principal as the audit log records it: a user, or a user through an API key, with
|
|
36
|
+
* the request's client details beside it. Anonymous requests act as the system.
|
|
37
|
+
*/
|
|
38
|
+
export declare function auditActorFor(principal: Principal | null, request?: {
|
|
39
|
+
headers: Headers;
|
|
40
|
+
requestId?: string | null | undefined;
|
|
41
|
+
}): AuditActor;
|
|
42
|
+
/** The grants a principal's role gives in a space, whatever kind of role it is. */
|
|
43
|
+
export declare function grantsIn(principal: Principal, spaceId: string): readonly string[];
|
|
44
|
+
/**
|
|
45
|
+
* What a principal can actually do in a space: the role's grants (everything, for a
|
|
46
|
+
* superadmin) narrowed by an API key's restriction, if the request came through one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function effectiveGrants(principal: Principal, spaceId: string): readonly string[];
|
|
49
|
+
export declare function can(principal: Principal | null, spaceId: string | null, permission: Permission$1, typeId?: string | null): boolean;
|
|
50
|
+
export declare function assertCan(principal: Principal | null, spaceId: string | null, permission: Permission$1, typeId?: string | null): void;
|
|
51
|
+
/**
|
|
52
|
+
* The content types a principal may perform an action on in a space, or `null` for
|
|
53
|
+
* every type — what a listing narrows its filter to.
|
|
54
|
+
*/
|
|
55
|
+
export declare function allowedTypeIds(principal: Principal | null, spaceId: string, permission: ContentPermission$1): string[] | null;
|
|
56
|
+
/** Roles used by field-level `readRoles`/`writeRoles` checks. */
|
|
57
|
+
export declare function actorRoles(principal: Principal | null, spaceId: string | null): string[];
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/api-key.d.ts
|
|
60
|
+
export interface IssuedApiKey {
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
/** The full secret, shown once at creation and never recoverable afterwards. */
|
|
64
|
+
key: string;
|
|
65
|
+
prefix: string;
|
|
66
|
+
}
|
|
67
|
+
export interface IssueApiKeyOptions {
|
|
68
|
+
expiresAt?: Date | undefined;
|
|
69
|
+
/** Spaces the key may act in. `null`/omitted issues an unrestricted key. */
|
|
70
|
+
spaceIds?: string[] | null | undefined;
|
|
71
|
+
/** Grants the key may use. `null`/omitted leaves the owner's role as the limit. */
|
|
72
|
+
permissions?: string[] | null | undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Takes a presented key apart. The secret is base64url and may itself contain `_`, so
|
|
76
|
+
* the key is not split on it: the prefix is a fixed twelve hex characters and the
|
|
77
|
+
* secret is whatever follows.
|
|
78
|
+
*/
|
|
79
|
+
export declare function parseApiKey(presented: string): {
|
|
80
|
+
prefix: string;
|
|
81
|
+
secret: string;
|
|
82
|
+
} | null;
|
|
83
|
+
/**
|
|
84
|
+
* Long-lived credentials for headless consumers.
|
|
85
|
+
*
|
|
86
|
+
* Keys are stored as a SHA-256 digest, never in plaintext, and are looked up by an
|
|
87
|
+
* indexed non-secret prefix so verification is one indexed read plus one constant-time
|
|
88
|
+
* comparison — not a scan-and-compare over every row.
|
|
89
|
+
*/
|
|
90
|
+
export declare class ApiKeyService {
|
|
91
|
+
private readonly db;
|
|
92
|
+
private readonly repos;
|
|
93
|
+
constructor(db: Database, repos: Repositories);
|
|
94
|
+
issue(userId: string, name: string, options?: IssueApiKeyOptions): Promise<IssuedApiKey>;
|
|
95
|
+
/**
|
|
96
|
+
* Deletes the row rather than clearing `enabled`: a revoked key is never listed again
|
|
97
|
+
* or re-enabled, so a disabled row is only a secret digest left lying around.
|
|
98
|
+
*/
|
|
99
|
+
revoke(id: string): Promise<void>;
|
|
100
|
+
list(userId: string): Promise<{
|
|
101
|
+
id: string;
|
|
102
|
+
name: string | null;
|
|
103
|
+
start: string | null;
|
|
104
|
+
enabled: boolean;
|
|
105
|
+
expiresAt: Date | null;
|
|
106
|
+
lastRequest: Date | null;
|
|
107
|
+
spaceIds: string[] | null;
|
|
108
|
+
permissions: string[] | null;
|
|
109
|
+
createdAt: Date;
|
|
110
|
+
}[]>;
|
|
111
|
+
resolve(presented: string): Promise<Principal | null>;
|
|
112
|
+
/** Removes expired keys; scheduled by the jobs package. */
|
|
113
|
+
pruneExpired(): Promise<number>;
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/password.d.ts
|
|
117
|
+
/**
|
|
118
|
+
* Argon2id, the current OWASP recommendation over bcrypt — which also silently truncates
|
|
119
|
+
* passwords at 72 bytes. One definition serves better-auth's own sign-in path and the
|
|
120
|
+
* accounts an administrator creates, so both write the same hash format.
|
|
121
|
+
*/
|
|
122
|
+
export declare function hashPassword(password: string): Promise<string>;
|
|
123
|
+
export declare function verifyPassword(stored: string, password: string): Promise<boolean>;
|
|
124
|
+
/** Matches better-auth's `minPasswordLength`, so a password set here signs in there. */
|
|
125
|
+
export declare const MIN_PASSWORD_LENGTH = 12;
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/user.service.d.ts
|
|
128
|
+
export type InstanceRole = 'superadmin' | 'editor';
|
|
129
|
+
export interface CreateUserInput {
|
|
130
|
+
name: string;
|
|
131
|
+
email: string;
|
|
132
|
+
password: string;
|
|
133
|
+
role: InstanceRole;
|
|
134
|
+
}
|
|
135
|
+
export interface UpdateUserInput {
|
|
136
|
+
name?: string | undefined;
|
|
137
|
+
email?: string | undefined;
|
|
138
|
+
}
|
|
139
|
+
/** A user row without anything a directory listing should not carry. */
|
|
140
|
+
export interface UserSummary {
|
|
141
|
+
id: string;
|
|
142
|
+
name: string;
|
|
143
|
+
email: string;
|
|
144
|
+
image: string | null;
|
|
145
|
+
role: string;
|
|
146
|
+
banned: boolean;
|
|
147
|
+
banReason: string | null;
|
|
148
|
+
createdAt: Date;
|
|
149
|
+
updatedAt: Date;
|
|
150
|
+
}
|
|
151
|
+
export interface UserDetail extends UserSummary {
|
|
152
|
+
memberships: Array<{
|
|
153
|
+
spaceId: string;
|
|
154
|
+
role: MembershipRow['role'];
|
|
155
|
+
space: SpaceRow;
|
|
156
|
+
}>;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Instance-wide user administration: the accounts, their instance role, and whether they
|
|
160
|
+
* may sign in at all. Space membership stays with `SpaceService`, because it is a
|
|
161
|
+
* property of the space.
|
|
162
|
+
*
|
|
163
|
+
* Every rule here exists to keep the instance reachable: an administrator cannot lock
|
|
164
|
+
* themself out, and the instance always keeps at least one superadmin.
|
|
165
|
+
*/
|
|
166
|
+
export declare class UserService {
|
|
167
|
+
private readonly repos;
|
|
168
|
+
constructor(repos: Repositories);
|
|
169
|
+
get(userId: string): Promise<UserDetail>;
|
|
170
|
+
list(pagination: {
|
|
171
|
+
limit: number;
|
|
172
|
+
offset: number;
|
|
173
|
+
}, search?: string): Promise<{
|
|
174
|
+
total: number;
|
|
175
|
+
limit: number;
|
|
176
|
+
offset: number;
|
|
177
|
+
items: UserSummary[];
|
|
178
|
+
}>;
|
|
179
|
+
create(input: CreateUserInput): Promise<UserSummary>;
|
|
180
|
+
update(userId: string, input: UpdateUserInput): Promise<UserSummary>;
|
|
181
|
+
/** Changing the instance role; the last superadmin cannot step down. */
|
|
182
|
+
setRole(userId: string, role: InstanceRole): Promise<UserSummary>;
|
|
183
|
+
/**
|
|
184
|
+
* A new password, and every session gone with the old one: whoever held the account
|
|
185
|
+
* before the reset does not keep it afterwards.
|
|
186
|
+
*/
|
|
187
|
+
setPassword(userId: string, password: string): Promise<void>;
|
|
188
|
+
/**
|
|
189
|
+
* The account holder changing their own password: the current one must be right, and
|
|
190
|
+
* unlike an administrator's reset the other sessions stay signed in, since the person
|
|
191
|
+
* holding the account is the one making the change.
|
|
192
|
+
*/
|
|
193
|
+
changePassword(userId: string, currentPassword: string, password: string): Promise<void>;
|
|
194
|
+
/** A banned user is signed out everywhere and refused on the next request. */
|
|
195
|
+
ban(actorId: string, userId: string, reason: string | null): Promise<UserSummary>;
|
|
196
|
+
unban(userId: string): Promise<UserSummary>;
|
|
197
|
+
delete(actorId: string, userId: string): Promise<void>;
|
|
198
|
+
/** Signs the user out of every device without touching the account. */
|
|
199
|
+
revokeSessions(userId: string): Promise<void>;
|
|
200
|
+
/** Accounts are instance-wide, so their entries carry no space. */
|
|
201
|
+
private audit;
|
|
202
|
+
private require;
|
|
203
|
+
private assertNotSelf;
|
|
204
|
+
/**
|
|
205
|
+
* Whatever happens to `userId`, one superadmin must remain — otherwise the instance
|
|
206
|
+
* has no one left who can create a space or manage users, and no way back.
|
|
207
|
+
*/
|
|
208
|
+
private assertNotLastSuperadmin;
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/index.d.ts
|
|
212
|
+
export type ManabloxAuth = ReturnType<typeof createAuth>;
|
|
213
|
+
/** better-auth, wired to the Drizzle schema. Sessions are rows, so concurrent devices
|
|
214
|
+
* each hold their own. */
|
|
215
|
+
export interface AuthCallbacks {
|
|
216
|
+
/** Runs after a user row is created, inside better-auth's own transaction path. */
|
|
217
|
+
onUserCreated?: (userId: string) => Promise<void>;
|
|
218
|
+
/**
|
|
219
|
+
* Whether the public sign-up endpoint may create an account right now. Absent means
|
|
220
|
+
* always. The host closes it once the first account exists, so every later account is
|
|
221
|
+
* created by an administrator rather than by whoever finds the login page.
|
|
222
|
+
*/
|
|
223
|
+
allowSignUp?: () => Promise<boolean>;
|
|
224
|
+
/** Runs after a session row is created: a sign-in. */
|
|
225
|
+
onSessionCreated?: (session: {
|
|
226
|
+
userId: string;
|
|
227
|
+
ipAddress?: string | null | undefined;
|
|
228
|
+
userAgent?: string | null | undefined;
|
|
229
|
+
}) => Promise<void>;
|
|
230
|
+
}
|
|
231
|
+
export declare function createAuth(config: AuthConfig, db: Database, callbacks?: AuthCallbacks): import("better-auth").Auth<{
|
|
232
|
+
secret: string;
|
|
233
|
+
baseURL?: string;
|
|
234
|
+
trustedOrigins: string[];
|
|
235
|
+
database: (options: import("better-auth").BetterAuthOptions) => import("better-auth").DBAdapter<import("better-auth").BetterAuthOptions>;
|
|
236
|
+
emailAndPassword: {
|
|
237
|
+
enabled: boolean;
|
|
238
|
+
minPasswordLength: number;
|
|
239
|
+
password: {
|
|
240
|
+
hash: typeof hashPassword;
|
|
241
|
+
verify: ({ hash: stored, password }: {
|
|
242
|
+
hash: string;
|
|
243
|
+
password: string;
|
|
244
|
+
}) => Promise<boolean>;
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
session: {
|
|
248
|
+
expiresIn: number;
|
|
249
|
+
updateAge: number;
|
|
250
|
+
cookieCache: {
|
|
251
|
+
enabled: true;
|
|
252
|
+
maxAge: number;
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
plugins: [{
|
|
256
|
+
id: "bearer";
|
|
257
|
+
version: string;
|
|
258
|
+
hooks: {
|
|
259
|
+
before: {
|
|
260
|
+
matcher(context: import("better-auth").HookEndpointContext): boolean;
|
|
261
|
+
handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<{
|
|
262
|
+
context: {
|
|
263
|
+
headers: Headers;
|
|
264
|
+
};
|
|
265
|
+
} | undefined>>;
|
|
266
|
+
}[];
|
|
267
|
+
after: {
|
|
268
|
+
matcher(context: import("better-auth").HookEndpointContext): true;
|
|
269
|
+
handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
|
|
270
|
+
}[];
|
|
271
|
+
};
|
|
272
|
+
options: import("better-auth/plugins").BearerOptions | undefined;
|
|
273
|
+
}];
|
|
274
|
+
databaseHooks: {
|
|
275
|
+
user: {
|
|
276
|
+
create: {
|
|
277
|
+
before: (user: {
|
|
278
|
+
id: string;
|
|
279
|
+
createdAt: Date;
|
|
280
|
+
updatedAt: Date;
|
|
281
|
+
email: string;
|
|
282
|
+
emailVerified: boolean;
|
|
283
|
+
name: string;
|
|
284
|
+
image?: string | null | undefined;
|
|
285
|
+
} & Record<string, unknown>) => Promise<{
|
|
286
|
+
data: {
|
|
287
|
+
id: string;
|
|
288
|
+
createdAt: Date;
|
|
289
|
+
updatedAt: Date;
|
|
290
|
+
email: string;
|
|
291
|
+
emailVerified: boolean;
|
|
292
|
+
name: string;
|
|
293
|
+
image?: string | null | undefined;
|
|
294
|
+
} & Record<string, unknown>;
|
|
295
|
+
}>;
|
|
296
|
+
after: (user: {
|
|
297
|
+
id: string;
|
|
298
|
+
createdAt: Date;
|
|
299
|
+
updatedAt: Date;
|
|
300
|
+
email: string;
|
|
301
|
+
emailVerified: boolean;
|
|
302
|
+
name: string;
|
|
303
|
+
image?: string | null | undefined;
|
|
304
|
+
} & Record<string, unknown>) => Promise<void>;
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
session: {
|
|
308
|
+
create: {
|
|
309
|
+
after: (session: {
|
|
310
|
+
id: string;
|
|
311
|
+
createdAt: Date;
|
|
312
|
+
updatedAt: Date;
|
|
313
|
+
userId: string;
|
|
314
|
+
expiresAt: Date;
|
|
315
|
+
token: string;
|
|
316
|
+
ipAddress?: string | null | undefined;
|
|
317
|
+
userAgent?: string | null | undefined;
|
|
318
|
+
} & Record<string, unknown>) => Promise<void>;
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
advanced: {
|
|
323
|
+
database: {
|
|
324
|
+
generateId: () => `${string}-${string}-${string}-${string}-${string}`;
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
}>;
|
|
328
|
+
/**
|
|
329
|
+
* Resolves a request's session into a `Principal`, including its space memberships.
|
|
330
|
+
* Returns `null` for anonymous requests rather than throwing — route guards decide.
|
|
331
|
+
*/
|
|
332
|
+
export declare function resolvePrincipal(auth: ManabloxAuth, repos: Repositories, headers: Headers, apiKeys?: ApiKeyService): Promise<Principal | null>;
|
|
333
|
+
/**
|
|
334
|
+
* Promotes the very first account to `superadmin` and grants it ownership of every
|
|
335
|
+
* existing space, so a fresh install is reachable.
|
|
336
|
+
*
|
|
337
|
+
* Called from better-auth's user-create hook rather than at startup, so it fires for an
|
|
338
|
+
* account created after the server is already running.
|
|
339
|
+
*/
|
|
340
|
+
export declare function promoteFirstUser(manablox: Manablox, repos: Repositories, userId: string): Promise<void>;
|
|
341
|
+
/** Covers an instance whose first account predates this behaviour. */
|
|
342
|
+
export declare function attachBootstrapOwner(manablox: Manablox, repos: Repositories): void;
|
|
343
|
+
//#endregion
|
|
344
|
+
export { ALL_PERMISSIONS, BUILT_IN_ROLES, type BuiltInRole, CONTENT_ACTIONS, type ContentAction, type ContentPermission, type Grant, PERMISSION_GROUPS, type Permission, type PermissionGroup, type SpaceRole, grantsCover, intersectGrants, isBuiltInRole, normaliseGrants, parseGrant, permissionsFor, typesCoveredBy };
|