@meistrari/auth-core 0.1.1 → 1.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/dist/index.mjs CHANGED
@@ -1,29 +1,12 @@
1
1
  import { createRemoteJWKSet, jwtVerify } from 'jose';
2
- import { createAuthClient as createAuthClient$1 } from 'better-auth/client';
3
- import { organizationClient } from 'better-auth/client/plugins';
2
+ import { createAuthClient } from 'better-auth/client';
3
+ import { organizationClient, twoFactorClient, jwtClient, adminClient, inferAdditionalFields } from 'better-auth/client/plugins';
4
4
  import { ssoClient } from '@better-auth/sso/client';
5
5
  import { createAccessControl } from 'better-auth/plugins/access';
6
6
  import { defaultStatements } from 'better-auth/plugins/organization/access';
7
7
 
8
- const version = "0.1.1";
8
+ const version = "1.1.0";
9
9
 
10
- function isTokenExpired(token) {
11
- const payload = JSON.parse(atob(token.split(".")[1] ?? ""));
12
- return payload.exp && Date.now() / 1e3 > payload.exp;
13
- }
14
- async function validateToken(token, apiUrl) {
15
- try {
16
- const JWKS = createRemoteJWKSet(
17
- new URL(`${apiUrl}/api/auth/jwks`)
18
- );
19
- await jwtVerify(token, JWKS, {
20
- issuer: apiUrl
21
- });
22
- return true;
23
- } catch (error) {
24
- return false;
25
- }
26
- }
27
10
  const statements = {
28
11
  ...defaultStatements,
29
12
  access: ["admin", "member", "reviewer"]
@@ -49,15 +32,54 @@ const rolesAccessControl = {
49
32
  access: ["reviewer"]
50
33
  })
51
34
  };
35
+ const userAdditionalFields = {
36
+ lastActiveAt: {
37
+ type: "date",
38
+ required: false,
39
+ defaultValue: null
40
+ }
41
+ };
42
+ const organizationAdditionalFields = {
43
+ settings: {
44
+ type: "json",
45
+ input: true,
46
+ required: false
47
+ }
48
+ };
49
+
50
+ class BaseError extends Error {
51
+ code;
52
+ constructor(code, message, options) {
53
+ super(message, options);
54
+ this.code = code;
55
+ }
56
+ }
57
+
58
+ class InvalidSocialProvider extends BaseError {
59
+ constructor(message) {
60
+ super("INVALID_SOCIAL_PROVIDER", message);
61
+ }
62
+ }
63
+ class InvalidCallbackURL extends BaseError {
64
+ constructor(message) {
65
+ super("INVALID_CALLBACK_URL", message);
66
+ }
67
+ }
68
+ class EmailRequired extends BaseError {
69
+ constructor(message) {
70
+ super("EMAIL_REQUIRED", message);
71
+ }
72
+ }
73
+
52
74
  const customEndpointsPluginClient = () => {
53
75
  return {
54
76
  id: "custom-endpoints",
55
77
  $InferServerPlugin: {}
56
78
  };
57
79
  };
58
- function createAuthClient(baseURL) {
59
- return createAuthClient$1({
60
- baseURL,
80
+ function createAPIClient(apiUrl, headers) {
81
+ return createAuthClient({
82
+ baseURL: apiUrl,
61
83
  plugins: [
62
84
  organizationClient({
63
85
  ac,
@@ -65,17 +87,469 @@ function createAuthClient(baseURL) {
65
87
  teams: {
66
88
  enabled: true
67
89
  }
90
+ // TODO: check if this is fixed in the next version
91
+ // schema: inferOrgAdditionalFields({
92
+ // organization: {
93
+ // additionalFields: {
94
+ // settings: {
95
+ // type: 'json' as const,
96
+ // input: true,
97
+ // required: false,
98
+ // }
99
+ // },
100
+ // }
101
+ // })
68
102
  }),
69
103
  customEndpointsPluginClient(),
70
- ssoClient()
104
+ ssoClient(),
105
+ twoFactorClient(),
106
+ jwtClient(),
107
+ adminClient(),
108
+ inferAdditionalFields({
109
+ user: userAdditionalFields
110
+ })
71
111
  ],
72
112
  fetchOptions: {
73
113
  credentials: "include",
74
114
  headers: {
75
- "User-Agent": `auth-sdk:${version}`
76
- }
115
+ "User-Agent": `auth-sdk:core:${version}`,
116
+ ...headers
117
+ },
118
+ throw: true
77
119
  }
78
120
  });
79
121
  }
122
+ createAPIClient("");
123
+
124
+ class OrganizationService {
125
+ /**
126
+ * Creates a new OrganizationService instance.
127
+ *
128
+ * @param client - The API client for making organization requests
129
+ */
130
+ constructor(client) {
131
+ this.client = client;
132
+ }
133
+ /**
134
+ * Retrieves a single organization by ID with optional related data.
135
+ *
136
+ * @param id - The organization ID
137
+ * @param options - Configuration for including related data
138
+ * @param options.includeMembers - Include organization members
139
+ * @param options.includeInvitations - Include pending invitations
140
+ * @param options.includeTeams - Include organization teams
141
+ * @returns The organization with optionally included related data
142
+ */
143
+ async getOrganization(id, options) {
144
+ const organizationPromise = this.client.organization.list({
145
+ query: {
146
+ id
147
+ }
148
+ });
149
+ const membersPromise = options?.includeMembers ? this.client.organization.listMembers({
150
+ query: {
151
+ organizationId: id
152
+ }
153
+ }) : void 0;
154
+ const invitationsPromise = options?.includeInvitations ? this.client.organization.listInvitations({
155
+ query: {
156
+ organizationId: id
157
+ }
158
+ }) : void 0;
159
+ const teamsPromise = options?.includeTeams ? this.client.organization.listTeams({
160
+ query: {
161
+ organizationId: id
162
+ }
163
+ }) : void 0;
164
+ const [organization, members, invitations, teams] = await Promise.all([organizationPromise, membersPromise, invitationsPromise, teamsPromise]);
165
+ return {
166
+ ...organization[0],
167
+ ...members && { members: members.members },
168
+ ...invitations && { invitations },
169
+ ...teams && { teams }
170
+ };
171
+ }
172
+ /**
173
+ * Lists all organizations accessible to the current user.
174
+ *
175
+ * @returns An array of organizations
176
+ */
177
+ async listOrganizations() {
178
+ const organizations = await this.client.organization.list();
179
+ return organizations;
180
+ }
181
+ /**
182
+ * Sets the active organization for the current user session.
183
+ *
184
+ * @param id - The organization ID to set as active
185
+ */
186
+ async setActiveOrganization(id) {
187
+ await this.client.organization.setActive({
188
+ organizationId: id
189
+ });
190
+ }
191
+ /**
192
+ * Updates an organization's details.
193
+ *
194
+ * @param payload - The organization fields to update
195
+ * @param payload.name - New organization name
196
+ * @param payload.logo - New organization logo URL
197
+ * @param payload.settings - New organization settings
198
+ * @returns The updated organization
199
+ */
200
+ async updateOrganization(payload) {
201
+ const organization = await this.client.organization.update({
202
+ data: {
203
+ ...payload,
204
+ logo: payload.logo ?? void 0
205
+ }
206
+ });
207
+ return organization;
208
+ }
209
+ /**
210
+ * Lists members of the active organization with optional pagination.
211
+ *
212
+ * @param options - Pagination options
213
+ * @param options.limit - Maximum number of members to return
214
+ * @param options.offset - Number of members to skip
215
+ * @returns An array of organization members
216
+ */
217
+ async listMembers(options) {
218
+ const members = await this.client.organization.listMembers({
219
+ query: options
220
+ });
221
+ return members.members;
222
+ }
223
+ /**
224
+ * Gets the currently active organization member for the authenticated user.
225
+ *
226
+ * @returns The active member object
227
+ */
228
+ async getActiveMember() {
229
+ return this.client.organization.getActiveMember();
230
+ }
231
+ /**
232
+ * Invites a user to join the active organization.
233
+ *
234
+ * @param options - Invitation configuration
235
+ * @param options.userEmail - Email address of the user to invite
236
+ * @param options.role - Role to assign to the invited user
237
+ * @param options.teamId - Team ID to add the user to
238
+ * @param options.resend - Whether to resend if invitation already exists
239
+ * @returns The created invitation
240
+ */
241
+ async inviteUserToOrganization({ userEmail, role, teamId, resend }) {
242
+ return this.client.organization.inviteMember({
243
+ email: userEmail,
244
+ role,
245
+ teamId,
246
+ resend: resend ?? false
247
+ });
248
+ }
249
+ /**
250
+ * Cancels a pending organization invitation.
251
+ *
252
+ * @param id - The invitation ID to cancel
253
+ */
254
+ async cancelInvitation(id) {
255
+ await this.client.organization.cancelInvitation({
256
+ invitationId: id
257
+ });
258
+ }
259
+ /**
260
+ * Accepts an organization invitation.
261
+ *
262
+ * @param id - The invitation ID to accept
263
+ */
264
+ async acceptInvitation(id) {
265
+ await this.client.organization.acceptInvitation({
266
+ invitationId: id
267
+ });
268
+ }
269
+ /**
270
+ * Removes a user from the active organization.
271
+ *
272
+ * @param options - User identifier (either memberId or userEmail must be provided)
273
+ * @param options.memberId - The member ID to remove
274
+ * @param options.userEmail - The user email to remove
275
+
276
+ */
277
+ async removeUserFromOrganization({ memberId, userEmail }) {
278
+ await this.client.organization.removeMember({
279
+ memberIdOrEmail: memberId ?? userEmail
280
+ });
281
+ }
282
+ /**
283
+ * Updates the role of an organization member.
284
+ *
285
+ * @param options - Role update configuration
286
+ * @param options.memberId - The member ID to update
287
+ * @param options.role - The new role to assign
288
+ */
289
+ async updateMemberRole({ memberId, role }) {
290
+ await this.client.organization.updateMemberRole({
291
+ memberId,
292
+ role
293
+ });
294
+ }
295
+ /**
296
+ * Creates a new team within the active organization.
297
+ *
298
+ * @param payload - Team configuration
299
+ * @param payload.name - The name of the team
300
+ * @returns The created team
301
+ */
302
+ async createTeam(payload) {
303
+ return this.client.organization.createTeam(payload);
304
+ }
305
+ /**
306
+ * Updates an existing team's details.
307
+ *
308
+ * @param id - The team ID to update
309
+ * @param payload - Team fields to update
310
+ * @param payload.name - The new team name
311
+ * @returns The updated team
312
+ */
313
+ async updateTeam(id, payload) {
314
+ return this.client.organization.updateTeam({
315
+ teamId: id,
316
+ data: payload
317
+ });
318
+ }
319
+ /**
320
+ * Deletes a team from the active organization.
321
+ *
322
+ * @param id - The team ID to delete
323
+ */
324
+ async deleteTeam(id) {
325
+ await this.client.organization.removeTeam({
326
+ teamId: id
327
+ });
328
+ }
329
+ /**
330
+ * Lists all teams in the active organization.
331
+ *
332
+ * @returns An array of teams
333
+ */
334
+ async listTeams() {
335
+ return this.client.organization.listTeams();
336
+ }
337
+ /**
338
+ * Lists all members of a specific team.
339
+ *
340
+ * @param id - The team ID
341
+ * @returns An array of team members
342
+ */
343
+ async listTeamMembers(id) {
344
+ return this.client.organization.listTeamMembers({
345
+ query: {
346
+ teamId: id
347
+ }
348
+ });
349
+ }
350
+ /**
351
+ * Adds a user to a team.
352
+ *
353
+ * @param teamId - The team ID
354
+ * @param userId - The user ID to add
355
+ */
356
+ async addTeamMember(teamId, userId) {
357
+ await this.client.organization.addTeamMember({
358
+ teamId,
359
+ userId
360
+ });
361
+ }
362
+ /**
363
+ * Removes a user from a team.
364
+ *
365
+ * @param teamId - The team ID
366
+ * @param userId - The user ID to remove
367
+ */
368
+ async removeTeamMember(teamId, userId) {
369
+ await this.client.organization.removeTeamMember({
370
+ teamId,
371
+ userId
372
+ });
373
+ }
374
+ }
375
+
376
+ function isValidUrl(url) {
377
+ try {
378
+ new URL(url);
379
+ return true;
380
+ } catch (error) {
381
+ return false;
382
+ }
383
+ }
384
+ class SessionService {
385
+ /**
386
+ * Creates a new SessionService instance.
387
+ *
388
+ * @param client - The API client for making authentication requests
389
+ * @param apiUrl - The base URL of the authentication API
390
+ */
391
+ constructor(client, apiUrl) {
392
+ this.client = client;
393
+ this.apiUrl = apiUrl;
394
+ }
395
+ /**
396
+ * Initiates social authentication flow with Google or Microsoft.
397
+ *
398
+ * @param options - Social sign-in configuration
399
+ * @param options.provider - The social provider
400
+ * @param options.callbackURL - URL to redirect to after successful authentication
401
+ * @param options.errorCallbackURL - URL to redirect to on error
402
+ *
403
+ * @throws {InvalidSocialProvider} When the provider is not 'google' or 'microsoft'
404
+ * @throws {InvalidCallbackURL} When callback URLs are malformed
405
+ */
406
+ async signInWithSocialProvider({
407
+ provider,
408
+ callbackURL,
409
+ errorCallbackURL
410
+ }) {
411
+ if (provider !== "google" && provider !== "microsoft") {
412
+ throw new InvalidSocialProvider("Invalid social provider");
413
+ }
414
+ if (!isValidUrl(callbackURL)) {
415
+ throw new InvalidCallbackURL(`Invalid callback URL: ${callbackURL}`);
416
+ }
417
+ if (errorCallbackURL && !isValidUrl(errorCallbackURL)) {
418
+ throw new InvalidCallbackURL(`Invalid error callback URL: ${errorCallbackURL}`);
419
+ }
420
+ const appUrl = encodeURIComponent(new URL(callbackURL).origin);
421
+ await this.client.signIn.social({
422
+ provider,
423
+ callbackURL,
424
+ errorCallbackURL: errorCallbackURL ?? `${this.apiUrl}/error?callbackURL=${appUrl}`
425
+ });
426
+ }
427
+ /**
428
+ * Initiates SAML-based Single Sign-On authentication flow.
429
+ *
430
+ * @param options - SAML sign-in configuration
431
+ * @param options.email - User's email to determine the SAML provider
432
+ * @param options.callbackURL - URL to redirect to after successful authentication
433
+ * @param options.errorCallbackURL - URL to redirect to on error
434
+ *
435
+ * @throws {EmailRequired} When email is not provided
436
+ * @throws {InvalidCallbackURL} When callback URLs are malformed
437
+ */
438
+ async signInWithSaml({
439
+ email,
440
+ callbackURL,
441
+ errorCallbackURL
442
+ }) {
443
+ if (!email) {
444
+ throw new EmailRequired("Email is required");
445
+ }
446
+ if (!isValidUrl(callbackURL)) {
447
+ throw new InvalidCallbackURL(`Invalid callback URL: ${callbackURL}`);
448
+ }
449
+ if (errorCallbackURL && !isValidUrl(errorCallbackURL)) {
450
+ throw new InvalidCallbackURL(`Invalid error callback URL: ${errorCallbackURL}`);
451
+ }
452
+ const appUrl = encodeURIComponent(new URL(callbackURL).origin);
453
+ await this.client.signIn.sso({
454
+ email,
455
+ callbackURL,
456
+ errorCallbackURL: errorCallbackURL ?? `${this.apiUrl}/error?callbackURL=${appUrl}`,
457
+ providerType: "saml"
458
+ });
459
+ }
460
+ /**
461
+ * Authenticates a user with email and password credentials.
462
+ *
463
+ * @param options - Email/password sign-in configuration
464
+ * @param options.email - User's email address
465
+ * @param options.password - User's password
466
+ */
467
+ async signInWithEmailAndPassword({
468
+ email,
469
+ password
470
+ }) {
471
+ await this.client.signIn.email({
472
+ email,
473
+ password
474
+ });
475
+ }
476
+ /**
477
+ * Signs out the currently authenticated user.
478
+ *
479
+ * @param callback - Callback function to execute after signing out
480
+ */
481
+ async signOut(callback) {
482
+ await this.client.signOut();
483
+ await callback?.();
484
+ }
485
+ /**
486
+ * Initiates a password reset request by sending a reset email.
487
+ *
488
+ * @param email - Email address of the user requesting password reset
489
+ * @param callbackURL - URL where the user will be redirected to complete the reset
490
+ */
491
+ async requestPasswordReset(email, callbackURL) {
492
+ await this.client.requestPasswordReset({
493
+ email,
494
+ redirectTo: callbackURL
495
+ });
496
+ }
497
+ /**
498
+ * Completes the password reset process with a reset token and new password.
499
+ *
500
+ * @param token - The password reset token from the email
501
+ * @param password - The new password to set
502
+ */
503
+ async resetPassword(token, password) {
504
+ await this.client.resetPassword({
505
+ token,
506
+ newPassword: password
507
+ });
508
+ }
509
+ }
510
+
511
+ class AuthClient {
512
+ client;
513
+ /**
514
+ * Session management service for authentication operations
515
+ */
516
+ session;
517
+ /**
518
+ * Organization management service for multi-tenant operations
519
+ */
520
+ organization;
521
+ /**
522
+ * Creates a new AuthClient instance.
523
+ *
524
+ * @param apiUrl - The base URL of the authentication API
525
+ * @param headers - Custom headers to include in all API requests
526
+ */
527
+ constructor(apiUrl, headers) {
528
+ this.client = createAPIClient(apiUrl, headers);
529
+ this.session = new SessionService(this.client, apiUrl);
530
+ this.organization = new OrganizationService(this.client);
531
+ }
532
+ }
533
+
534
+ function isTokenExpired(token) {
535
+ const payload = JSON.parse(atob(token.split(".")[1] ?? ""));
536
+ return payload.exp && Date.now() / 1e3 > payload.exp;
537
+ }
538
+ async function validateToken(token, apiUrl) {
539
+ if (isTokenExpired(token)) {
540
+ return false;
541
+ }
542
+ try {
543
+ const JWKS = createRemoteJWKSet(
544
+ new URL(`${apiUrl}/api/auth/jwks`)
545
+ );
546
+ await jwtVerify(token, JWKS, {
547
+ issuer: apiUrl
548
+ });
549
+ return true;
550
+ } catch (error) {
551
+ return false;
552
+ }
553
+ }
80
554
 
81
- export { Roles, ac, createAuthClient, isTokenExpired, rolesAccessControl, validateToken };
555
+ export { AuthClient, Roles, ac, isTokenExpired, organizationAdditionalFields, rolesAccessControl, userAdditionalFields, validateToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/auth-core",
3
- "version": "0.1.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -17,9 +17,11 @@
17
17
  "build": "unbuild"
18
18
  },
19
19
  "dependencies": {
20
- "@better-auth/sso": "1.3.28",
21
- "better-auth": "1.3.28",
22
- "jose": "6.1.0"
20
+ "@better-auth/sso": "1.4.1",
21
+ "better-auth": "1.4.1",
22
+ "jose": "6.1.0",
23
+ "nanostores": "1.0.1",
24
+ "@better-fetch/fetch": "1.1.18"
23
25
  },
24
26
  "devDependencies": {
25
27
  "@types/node": "latest",