@lobb-js/lobb-ext-auth 0.13.0 → 0.14.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/auth.js CHANGED
@@ -17,7 +17,7 @@ export class Auth {
17
17
  async login(payload) {
18
18
  const response = await this.utils.lobb.request({
19
19
  method: "POST",
20
- route: "/api/collections/auth_sessions",
20
+ route: "/api/actions/auth_login",
21
21
  payload: {
22
22
  data: payload,
23
23
  },
@@ -41,8 +41,8 @@ export class Auth {
41
41
  }
42
42
  async logout() {
43
43
  await this.utils.lobb.request({
44
- method: "DELETE",
45
- route: "/api/extensions/auth/logout",
44
+ method: "POST",
45
+ route: "/api/actions/auth_logout",
46
46
  });
47
47
  localStorage.removeItem("lobb_session");
48
48
  const currentPath = window.location.pathname;
@@ -12,7 +12,7 @@ async function syncincAdminUserInDB(lobb: Lobb, extensionConfig: ExtensionConfig
12
12
  // syncinc the admin user in users collection
13
13
  const config = extensionConfig;
14
14
  const { email, password, ...extraFields } = config.admin;
15
- const entries = (await lobb.collectionService.findAll({
15
+ const entries = (await lobb.collectionStore.findAll({
16
16
  collectionName: "auth_users",
17
17
  params: {
18
18
  filter: {
@@ -23,7 +23,7 @@ async function syncincAdminUserInDB(lobb: Lobb, extensionConfig: ExtensionConfig
23
23
  })).data;
24
24
  const adminUser = entries[0];
25
25
  if (!adminUser) {
26
- await lobb.collectionService.createOne({
26
+ await lobb.collectionStore.createOne({
27
27
  collectionName: "auth_users",
28
28
  data: {
29
29
  ...extraFields,
@@ -39,7 +39,7 @@ async function syncincAdminUserInDB(lobb: Lobb, extensionConfig: ExtensionConfig
39
39
  );
40
40
 
41
41
  if (adminUser.email !== email || !passwordIdentical) {
42
- await lobb.collectionService.updateOne({
42
+ await lobb.collectionStore.updateOne({
43
43
  collectionName: "auth_users",
44
44
  id: adminUser.id,
45
45
  data: {
@@ -5,12 +5,14 @@ import { collections } from "./collections/collections.ts";
5
5
  import { meta } from "./meta/meta.ts";
6
6
  import { migrations } from "./database/migrations.ts";
7
7
  import { getWorkflows } from "./workflows/index.ts";
8
+ import { getAuthActions } from "./workflows/actions.ts";
8
9
 
9
10
  export default function auth(extensionConfig: ExtensionConfig): Extension {
10
11
  return {
11
12
  name: "auth",
12
13
  icon: "Key",
13
14
  collections: (lobb) => collections(lobb, extensionConfig),
15
+ actions: getAuthActions(extensionConfig),
14
16
  migrations: migrations,
15
17
  meta: (lobb) => meta(lobb, extensionConfig),
16
18
  workflows: getWorkflows(extensionConfig),
@@ -35,7 +35,7 @@ export const openapi = {
35
35
  },
36
36
  },
37
37
  paths: {
38
- "/api/extensions/auth/login": {
38
+ "/api/actions/auth_login": {
39
39
  post: {
40
40
  tags: [
41
41
  "auth",
@@ -223,8 +223,8 @@ export const openapi = {
223
223
  },
224
224
  },
225
225
  },
226
- "/api/extensions/auth/logout": {
227
- delete: {
226
+ "/api/actions/auth_logout": {
227
+ post: {
228
228
  tags: [
229
229
  "auth",
230
230
  ],
@@ -26,7 +26,7 @@ export class Auth {
26
26
  public async login(payload: LoginPayload) {
27
27
  const response = await this.utils.lobb.request({
28
28
  method: "POST",
29
- route: "/api/collections/auth_sessions",
29
+ route: "/api/actions/auth_login",
30
30
  payload: {
31
31
  data: payload,
32
32
  },
@@ -55,8 +55,8 @@ export class Auth {
55
55
 
56
56
  public async logout() {
57
57
  await this.utils.lobb.request({
58
- method: "DELETE",
59
- route: "/api/extensions/auth/logout",
58
+ method: "POST",
59
+ route: "/api/actions/auth_logout",
60
60
  });
61
61
  localStorage.removeItem("lobb_session");
62
62
  const currentPath = window.location.pathname;
@@ -0,0 +1,34 @@
1
+ import type { Workflow } from "@lobb-js/core";
2
+ import type { Context } from "hono";
3
+ import { getBearerToken } from "../utils.ts";
4
+
5
+ // `auth_logout` needs the session token, which arrives as a bearer header. Read
6
+ // it off the Hono context here and pass it to the action (keeping the action
7
+ // pure + internally callable with an explicit token), then return 204.
8
+ //
9
+ // `auth_login` needs no HTTP adaptation — its JSON body { email, password } maps
10
+ // straight to the default action flow.
11
+
12
+ export function getAuthActionControllerWorkflows(): Workflow[] {
13
+ return [
14
+ {
15
+ name: "authLogoutActionController",
16
+ eventName: "core.actions.controller.override",
17
+ handler: async (input, ctx) => {
18
+ if (input.name !== "auth_logout") return;
19
+
20
+ const context = input.context as Context;
21
+ const token = getBearerToken(context);
22
+ if (!token) {
23
+ throw new ctx.LobbError({
24
+ code: "BAD_REQUEST",
25
+ message: "You should pass session's token through the bearer header",
26
+ });
27
+ }
28
+
29
+ await ctx.lobb.runAction("auth_logout", { token });
30
+ return context.body(null, 204);
31
+ },
32
+ },
33
+ ];
34
+ }
@@ -0,0 +1,89 @@
1
+ import type { ActionsConfig } from "@lobb-js/core";
2
+ import { LobbError } from "@lobb-js/core";
3
+ import type { ExtensionConfig, User } from "../config/extensionConfigSchema.ts";
4
+ import { generateRandomId } from "../utils.ts";
5
+ import { verify } from "argon2";
6
+
7
+ // Login and logout used to hijack the auth_sessions CRUD endpoints (create =
8
+ // login, deleteMany = logout), which made it impossible to manage session rows
9
+ // normally. They're now first-class actions; the auth_sessions collection is a
10
+ // plain CRUD collection again.
11
+
12
+ export function getAuthActions(_extensionConfig: ExtensionConfig): ActionsConfig {
13
+ return {
14
+ // POST /api/actions/auth_login { data: { email, password } }
15
+ auth_login: {
16
+ handler: async (input: any, ctx) => {
17
+ const email = input.data?.email ?? input.email;
18
+ const password = input.data?.password ?? input.password;
19
+
20
+ const users = await ctx.lobb.collectionStore.findAll({
21
+ collectionName: "auth_users",
22
+ params: { filter: { email } },
23
+ });
24
+
25
+ if (!users.data.length) {
26
+ throw new LobbError({
27
+ code: "NOT_FOUND",
28
+ message: `The user with this email (${email}) doesnt exist.`,
29
+ });
30
+ }
31
+
32
+ const user = users.data[0] as User;
33
+
34
+ const passwordIsCorrect = await verify(user.password, password);
35
+ if (!passwordIsCorrect) {
36
+ throw new LobbError({
37
+ code: "UNAUTHORIZED",
38
+ message: "The password provided is incorrect. Please verify and try again.",
39
+ });
40
+ }
41
+
42
+ const sessionPayload = {
43
+ token: generateRandomId(),
44
+ expires_at: new Date(Date.now() + 3600 * 1000),
45
+ user_id: user.id,
46
+ };
47
+
48
+ const sessionResult = await ctx.lobb.collectionStore.createOne({
49
+ collectionName: "auth_sessions",
50
+ data: sessionPayload,
51
+ });
52
+
53
+ return {
54
+ data: {
55
+ access_token: {
56
+ token: sessionPayload.token,
57
+ expires_at: sessionResult.data.expires_at,
58
+ },
59
+ user: {
60
+ id: user.id,
61
+ email: user.email,
62
+ role: user.role,
63
+ },
64
+ },
65
+ };
66
+ },
67
+ },
68
+
69
+ // POST /api/actions/auth_logout (bearer header → resolved to { token })
70
+ auth_logout: {
71
+ handler: async (input: any, ctx) => {
72
+ const token = input.token;
73
+ if (!token) {
74
+ throw new LobbError({
75
+ code: "BAD_REQUEST",
76
+ message: "You should pass session's token through the bearer header",
77
+ });
78
+ }
79
+
80
+ await ctx.lobb.collectionStore.deleteMany({
81
+ collectionName: "auth_sessions",
82
+ filter: { token },
83
+ });
84
+
85
+ return { success: true };
86
+ },
87
+ },
88
+ };
89
+ }
@@ -1,16 +1,16 @@
1
1
  import type { Workflow } from "@lobb-js/core";
2
2
  import type { Context } from "hono";
3
- import type { ExtensionConfig, User } from "../config/extensionConfigSchema.ts";
4
- import { generateRandomId, getBearerToken } from "../utils.ts";
5
- import { verify } from "argon2";
3
+ import type { ExtensionConfig } from "../config/extensionConfigSchema.ts";
4
+ import { getBearerToken } from "../utils.ts";
6
5
 
7
6
  export function getBaseWorkflows(_extensionConfig: ExtensionConfig): Workflow[] {
8
7
  return [
9
8
  {
10
9
  name: "auth_preventAdminUserDeletion",
11
- eventName: "core.service.preDeleteOne",
10
+ eventName: "core.store.preDeleteOne",
12
11
  handler: async (input, ctx) => {
13
- if (input.collectionName !== "auth_users") return;
12
+ if (input.collectionName !== "auth_users") return input;
13
+ if (input.triggeredBy !== "API") return input;
14
14
 
15
15
  if (Number(input.id) === 1) {
16
16
  throw new ctx.LobbError({
@@ -18,13 +18,15 @@ export function getBaseWorkflows(_extensionConfig: ExtensionConfig): Workflow[]
18
18
  message: "The admin user cannot be deleted.",
19
19
  });
20
20
  }
21
+ return input;
21
22
  },
22
23
  },
23
24
  {
24
25
  name: "auth_preventAdminUserUpdate",
25
- eventName: "core.service.preUpdateOne",
26
+ eventName: "core.store.preUpdateOne",
26
27
  handler: async (input, ctx) => {
27
- if (input.collectionName !== "auth_users") return;
28
+ if (input.collectionName !== "auth_users") return input;
29
+ if (input.triggeredBy !== "API") return input;
28
30
 
29
31
  if (Number(input.id) === 1) {
30
32
  throw new ctx.LobbError({
@@ -32,6 +34,7 @@ export function getBaseWorkflows(_extensionConfig: ExtensionConfig): Workflow[]
32
34
  message: "The admin user cannot be updated.",
33
35
  });
34
36
  }
37
+ return input;
35
38
  },
36
39
  },
37
40
  ...baseWorkflows,
@@ -53,7 +56,7 @@ export const baseWorkflows: Workflow[] = [
53
56
  const context = input.context as Context;
54
57
 
55
58
  // 1) Try the token as a normal user session.
56
- const sessionsResult = await ctx.workflows.collectionService({
59
+ const sessionsResult = await ctx.workflows.collectionStore({
57
60
  method: "findAll",
58
61
  props: {
59
62
  collectionName: "auth_sessions",
@@ -63,7 +66,7 @@ export const baseWorkflows: Workflow[] = [
63
66
  const session = sessionsResult.data[0];
64
67
 
65
68
  if (session) {
66
- const usersResult = await ctx.workflows.collectionService({
69
+ const usersResult = await ctx.workflows.collectionStore({
67
70
  method: "findAll",
68
71
  props: {
69
72
  collectionName: "auth_users",
@@ -79,7 +82,7 @@ export const baseWorkflows: Workflow[] = [
79
82
 
80
83
  // 2) Otherwise try the token as a share. Shares carry their own
81
84
  // permissions snapshot and have a fixed expiry — no user identity.
82
- const sharesResult = await ctx.workflows.collectionService({
85
+ const sharesResult = await ctx.workflows.collectionStore({
83
86
  method: "findAll",
84
87
  props: {
85
88
  collectionName: "auth_shares",
@@ -102,114 +105,4 @@ export const baseWorkflows: Workflow[] = [
102
105
  return input;
103
106
  },
104
107
  },
105
- // auth_logins workflows
106
- {
107
- name: "auth_userLoginsHandler",
108
- eventName: "core.controllers.preCreateOne",
109
- handler: async (input, ctx) => {
110
- if (input.collectionName === "auth_sessions") {
111
- const payloadEmail = input.body.data.email;
112
- const payloadPassword = input.body.data.password;
113
-
114
- const users = await ctx.workflows.collectionService({
115
- method: "findAll",
116
- props: {
117
- collectionName: "auth_users",
118
- params: {
119
- filter: {
120
- email: payloadEmail,
121
- },
122
- },
123
- },
124
- });
125
-
126
- if (!users.data.length) {
127
- throw new ctx.LobbError({
128
- code: "NOT_FOUND",
129
- message: `The user with this email (${payloadEmail}) doesnt exist.`,
130
- });
131
- }
132
-
133
- const user = users.data[0] as User;
134
- const hashedPassword = user.password;
135
-
136
- const passwordIsCorrect = await verify(
137
- hashedPassword,
138
- payloadPassword,
139
- );
140
- if (!passwordIsCorrect) {
141
- throw new ctx.LobbError({
142
- code: "UNAUTHORIZED",
143
- message:
144
- "The password provided is incorrect. Please verify and try again.",
145
- });
146
- }
147
-
148
- const sessionPayload = {
149
- token: generateRandomId(),
150
- expires_at: new Date(Date.now() + 3600 * 1000),
151
- user_id: user.id,
152
- };
153
-
154
- const sessionResult = await ctx.workflows.collectionService({
155
- method: "createOne",
156
- props: {
157
- collectionName: "auth_sessions",
158
- data: sessionPayload,
159
- },
160
- });
161
-
162
- throw new Response(
163
- JSON.stringify({
164
- data: {
165
- access_token: {
166
- token: sessionPayload.token,
167
- expires_at: sessionResult.data.expires_at,
168
- },
169
- user: {
170
- id: user.id,
171
- email: user.email,
172
- role: user.role,
173
- },
174
- },
175
- }),
176
- );
177
- }
178
- },
179
- },
180
- {
181
- name: "auth_userLogoutHandler",
182
- eventName: "core.controllers.preDeleteMany",
183
- handler: async (input, ctx) => {
184
- if (input.collectionName === "auth_sessions") {
185
- const context = input.context as Context;
186
- const token = getBearerToken(context);
187
-
188
- if (!token) {
189
- throw new ctx.LobbError({
190
- code: "BAD_REQUEST",
191
- message:
192
- "You should pass session's token through the bearer header",
193
- });
194
- }
195
-
196
- await ctx.workflows.collectionService({
197
- method: "deleteMany",
198
- props: {
199
- collectionName: "auth_sessions",
200
- filter: {
201
- token: token,
202
- },
203
- },
204
- });
205
-
206
- throw new Response(
207
- null,
208
- {
209
- status: 204,
210
- },
211
- );
212
- }
213
- },
214
- },
215
108
  ];
@@ -5,6 +5,7 @@ import { meAliasWorkflows } from "./meAliasWorkflows.ts";
5
5
  import { getCurrentUserPermissionsWorkflow } from "./currentUserPermissionsWorkflow.ts";
6
6
  import { getBaseWorkflows } from "./baseWorkflow.ts";
7
7
  import { getSharesWorkflows } from "./sharesWorkflows.ts";
8
+ import { getAuthActionControllerWorkflows } from "./actionController.ts";
8
9
  import { init } from "../database/init.ts";
9
10
  export function getWorkflows(extensionConfig: ExtensionConfig): Workflow[] {
10
11
  return [
@@ -17,6 +18,7 @@ export function getWorkflows(extensionConfig: ExtensionConfig): Workflow[] {
17
18
  },
18
19
  ...getBaseWorkflows(extensionConfig),
19
20
  ...getSharesWorkflows(extensionConfig),
21
+ ...getAuthActionControllerWorkflows(),
20
22
 
21
23
  // TODO: think about putting the below workflows above at the beggining
22
24
  // this will give us the ability to specify which roles have the ability to login, register etc
@@ -26,15 +26,16 @@ export function getSharesWorkflows(extensionConfig: ExtensionConfig): Workflow[]
26
26
  },
27
27
  },
28
28
  // Normalize the two ways a caller can specify share lifetime into the
29
- // single `expires_at` column the rest of the system uses. Runs at the
30
- // service layer, which fires before the store-level required-field
31
- // check — so `expires_at` can stay schema-required while still letting
32
- // callers send only `expires_in_seconds`. Keeps an explicit "neither
33
- // provided" throw for a friendlier error message than the generic
34
- // required-field error.
29
+ // single `expires_at` column the rest of the system uses. `order: "pre"`
30
+ // makes it run before the unordered subscribers of preCreateOne — crucially
31
+ // before the required-field check — so `expires_at` can stay schema-required
32
+ // while still letting callers send only `expires_in_seconds`. Keeps an
33
+ // explicit "neither provided" throw for a friendlier error message than the
34
+ // generic required-field error.
35
35
  {
36
36
  name: "auth_normalizeShareExpiry",
37
- eventName: "core.service.preCreateOne",
37
+ eventName: "core.store.preCreateOne",
38
+ order: "pre",
38
39
  handler: async (input, ctx) => {
39
40
  if (input.collectionName !== "auth_shares") return input;
40
41
 
@@ -82,7 +83,7 @@ export function getSharesWorkflows(extensionConfig: ExtensionConfig): Workflow[]
82
83
  // subset of the creator's own permissions. Without this, any user who can
83
84
  // create rows in auth_shares could mint a token that grants admin-level
84
85
  // access. Only runs for API-triggered creates — internal callers (server
85
- // code, tests using collectionService directly) are trusted.
86
+ // code, tests using collectionStore directly) are trusted.
86
87
  {
87
88
  name: "auth_validateShareSubset",
88
89
  eventName: "core.store.preCreateOne",
@@ -125,7 +126,7 @@ export function getSharesWorkflows(extensionConfig: ExtensionConfig): Workflow[]
125
126
  name: "auth_cleanupExpiredShares",
126
127
  eventName: "0 3 * * *",
127
128
  handler: async (_input, ctx) => {
128
- await ctx.lobb.collectionService.deleteMany({
129
+ await ctx.lobb.collectionStore.deleteMany({
129
130
  collectionName: "auth_shares",
130
131
  filter: { expires_at: { $lt: new Date().toISOString() } },
131
132
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobb-js/lobb-ext-auth",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -32,12 +32,12 @@
32
32
  "package": "svelte-package --input extensions/auth/studio"
33
33
  },
34
34
  "dependencies": {
35
- "@lobb-js/core": "^0.40.0",
35
+ "@lobb-js/core": "^0.41.0",
36
36
  "argon2": "^0.40.3",
37
37
  "hono": "^4.7.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@lobb-js/studio": "^0.47.0",
40
+ "@lobb-js/studio": "^0.49.0",
41
41
  "@lucide/svelte": "^0.563.1",
42
42
  "@sveltejs/adapter-node": "^5.5.4",
43
43
  "@sveltejs/kit": "^2.60.1",