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