@lobb-js/lobb-ext-auth 0.10.4 → 0.11.1

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.
Files changed (56) hide show
  1. package/README.md +1 -0
  2. package/dist/auth.d.ts +2 -1
  3. package/dist/auth.js +23 -4
  4. package/dist/index.js +41 -2
  5. package/dist/lib/components/pages/settings/index.svelte +1 -1
  6. package/dist/lib/components/pages/settings/pages/activityFeed.svelte +1 -1
  7. package/dist/lib/components/pages/settings/pages/rolesAndPermissions.svelte +1 -1
  8. package/dist/lib/components/pages/settings/pages/users.svelte +1 -1
  9. package/dist/lib/components/pages/userSettings/index.svelte +45 -32
  10. package/dist/onStartup.js +17 -2
  11. package/extensions/auth/collections/collections.ts +2 -0
  12. package/extensions/auth/collections/shares.ts +60 -0
  13. package/extensions/auth/config/extensionConfigSchema.d.ts +41 -0
  14. package/extensions/auth/config/permissionsAction/create.d.ts +18 -0
  15. package/extensions/auth/config/permissionsAction/delete.d.ts +3 -0
  16. package/extensions/auth/config/permissionsAction/read.d.ts +11 -0
  17. package/extensions/auth/config/permissionsAction/update.d.ts +18 -0
  18. package/extensions/auth/index.ts +0 -2
  19. package/extensions/auth/permissions.d.ts +2 -0
  20. package/extensions/auth/permissions.ts +34 -0
  21. package/extensions/auth/studio/auth.ts +25 -5
  22. package/extensions/auth/studio/index.ts +44 -2
  23. package/extensions/auth/studio/lib/components/pages/settings/index.svelte +1 -1
  24. package/extensions/auth/studio/lib/components/pages/settings/pages/activityFeed.svelte +1 -1
  25. package/extensions/auth/studio/lib/components/pages/settings/pages/rolesAndPermissions.svelte +1 -1
  26. package/extensions/auth/studio/lib/components/pages/settings/pages/users.svelte +1 -1
  27. package/extensions/auth/studio/lib/components/pages/userSettings/index.svelte +45 -32
  28. package/extensions/auth/studio/onStartup.ts +14 -2
  29. package/extensions/auth/tests/collections/shares.test.ts +657 -0
  30. package/extensions/auth/tests/configs/auth.ts +17 -0
  31. package/extensions/auth/tests/controllers/me.test.ts +104 -0
  32. package/extensions/auth/tests/permissions.test.ts +127 -0
  33. package/extensions/auth/tests/workflows/shareIntersection.test.ts +158 -0
  34. package/extensions/auth/workflows/baseWorkflow.ts +48 -26
  35. package/extensions/auth/workflows/currentUserPermissionsWorkflow.ts +32 -0
  36. package/extensions/auth/workflows/index.ts +12 -0
  37. package/extensions/auth/workflows/meAliasWorkflows.ts +26 -0
  38. package/extensions/auth/workflows/policiesWorkflows.ts +64 -117
  39. package/extensions/auth/workflows/shareIntersection.ts +64 -0
  40. package/extensions/auth/workflows/sharesWorkflows.ts +135 -0
  41. package/extensions/auth/workflows/utils.ts +132 -224
  42. package/package.json +4 -6
  43. package/dist/lib/components/pages/userSettings/components/account.svelte +0 -106
  44. package/dist/lib/components/pages/userSettings/components/account.svelte.d.ts +0 -14
  45. package/dist/lib/components/pages/userSettings/components/profile.svelte +0 -87
  46. package/dist/lib/components/pages/userSettings/components/profile.svelte.d.ts +0 -14
  47. package/dist/tests/login.spec.d.ts +0 -1
  48. package/dist/tests/login.spec.js +0 -27
  49. package/dist/tests/package.json +0 -1
  50. package/dist/tests/playwright.config.cjs +0 -27
  51. package/dist/tests/playwright.config.d.cts +0 -2
  52. package/extensions/auth/studio/lib/components/pages/userSettings/components/account.svelte +0 -106
  53. package/extensions/auth/studio/lib/components/pages/userSettings/components/profile.svelte +0 -87
  54. package/extensions/auth/studio/tests/login.spec.ts +0 -34
  55. package/extensions/auth/studio/tests/package.json +0 -1
  56. package/extensions/auth/studio/tests/playwright.config.cjs +0 -27
@@ -269,4 +269,108 @@ describe("Login", () => {
269
269
 
270
270
  expect(response3.status).toEqual(200);
271
271
  });
272
+
273
+ describe("permissions in the /me response", () => {
274
+ async function login(email: string, password: string): Promise<string> {
275
+ const res = await fetch(`${baseUrl}/api/collections/auth_sessions`, {
276
+ method: "POST",
277
+ headers: { "Content-Type": "application/json" },
278
+ body: JSON.stringify({ data: { email, password } }),
279
+ });
280
+ const body = await res.json();
281
+ return body.data.access_token.token;
282
+ }
283
+
284
+ async function createUser(email: string, password: string, role: string) {
285
+ await fetch(`${baseUrl}/api/collections/auth_users`, {
286
+ method: "POST",
287
+ headers: { "Content-Type": "application/json" },
288
+ body: JSON.stringify({ data: { email, password, role } }),
289
+ });
290
+ }
291
+
292
+ it("should attach the role's permissions as a sibling of data for the current user via /me", async () => {
293
+ await createUser("perm_author@example.com", "pw", "author");
294
+ const token = await login("perm_author@example.com", "pw");
295
+
296
+ const res = await fetch(`${baseUrl}/api/collections/auth_users/me`, {
297
+ headers: { "Authorization": `Bearer ${token}` },
298
+ });
299
+ const body = await res.json();
300
+
301
+ expect(res.status).toEqual(200);
302
+ expect(body.data.permissions).toBeUndefined();
303
+ // author role config has auth_users.read/update/delete — function-typed
304
+ // filters get stripped to plain JSON before being returned.
305
+ expect(body.permissions).toEqual({
306
+ auth_users: {
307
+ read: { filter: {} },
308
+ update: true,
309
+ delete: true,
310
+ },
311
+ articles: {
312
+ read: {
313
+ filter: {},
314
+ fields: { id: true, title: true, body: true },
315
+ },
316
+ create: { fields: { title: true, body: true } },
317
+ },
318
+ auth_shares: {
319
+ create: true,
320
+ },
321
+ });
322
+ });
323
+
324
+ it("should not attach permissions when reading another user's row", async () => {
325
+ // login as admin so we can read other users
326
+ const adminToken = await login("admin@test.com", "admin");
327
+
328
+ // fetch a non-admin user's row by id
329
+ const otherUser = (await lobb.collectionService.findAll({
330
+ collectionName: "auth_users",
331
+ params: { filter: { email: "perm_author@example.com" } },
332
+ })).data[0];
333
+
334
+ const res = await fetch(
335
+ `${baseUrl}/api/collections/auth_users/${otherUser.id}`,
336
+ { headers: { "Authorization": `Bearer ${adminToken}` } },
337
+ );
338
+ const body = await res.json();
339
+
340
+ expect(res.status).toEqual(200);
341
+ expect(body.permissions).toBeUndefined();
342
+ });
343
+
344
+ it("should not attach permissions when the current user reads their own row by id (not via /me)", async () => {
345
+ const token = await login("perm_author@example.com", "pw");
346
+ const ownUser = (await lobb.collectionService.findAll({
347
+ collectionName: "auth_users",
348
+ params: { filter: { email: "perm_author@example.com" } },
349
+ })).data[0];
350
+
351
+ const res = await fetch(
352
+ `${baseUrl}/api/collections/auth_users/${ownUser.id}`,
353
+ { headers: { "Authorization": `Bearer ${token}` } },
354
+ );
355
+ const body = await res.json();
356
+
357
+ expect(res.status).toEqual(200);
358
+ expect(body.permissions).toBeUndefined();
359
+ });
360
+
361
+ it("should allow a user whose role has no auth_users.read permission to fetch /me", async () => {
362
+ // The "reader" role in authConfig has only articles.read — no auth_users
363
+ // perm at all. /me should still work because the user is reading their
364
+ // own row, but historically the internal findOne→findAll re-checked the
365
+ // policy on preFindAll and threw 403 for roles lacking auth_users.read.
366
+ await createUser("no_auth_perm@example.com", "pw", "reader");
367
+ const token = await login("no_auth_perm@example.com", "pw");
368
+
369
+ const res = await fetch(`${baseUrl}/api/collections/auth_users/me`, {
370
+ headers: { "Authorization": `Bearer ${token}` },
371
+ });
372
+
373
+ expect(res.status).toEqual(200);
374
+ });
375
+ });
272
376
  });
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { isActionAllowed } from "../permissions.ts";
3
+
4
+ // Default-deny is the contract: only explicit `true` or an object that
5
+ // actually lists at least one constraint counts as a grant. Anything empty
6
+ // (`undefined`, `{}`, `null`) at ANY level — role / collection / action —
7
+ // must collapse to "no grant". These tests pin that invariant so a future
8
+ // refactor can't accidentally turn `{}` back into "allow".
9
+ describe("isActionAllowed — empty {} means false at every level", () => {
10
+ describe("role permissions level", () => {
11
+ it("returns false for undefined permissions", () => {
12
+ expect(isActionAllowed("articles", "read", undefined)).toBe(false);
13
+ expect(isActionAllowed("articles", "create", undefined)).toBe(false);
14
+ expect(isActionAllowed("articles", "update", undefined)).toBe(false);
15
+ expect(isActionAllowed("articles", "delete", undefined)).toBe(false);
16
+ });
17
+
18
+ it("returns false for an empty {} permissions object", () => {
19
+ expect(isActionAllowed("articles", "read", {})).toBe(false);
20
+ expect(isActionAllowed("articles", "create", {})).toBe(false);
21
+ expect(isActionAllowed("articles", "update", {})).toBe(false);
22
+ expect(isActionAllowed("articles", "delete", {})).toBe(false);
23
+ });
24
+
25
+ it("returns true only for the explicit `true` shortcut (admin)", () => {
26
+ expect(isActionAllowed("articles", "read", true)).toBe(true);
27
+ expect(isActionAllowed("any_collection_name", "delete", true)).toBe(true);
28
+ });
29
+ });
30
+
31
+ describe("collection level", () => {
32
+ it("returns false when the collection is missing from permissions", () => {
33
+ const perms = { articles: { read: true } };
34
+ expect(isActionAllowed("auth_users", "read", perms)).toBe(false);
35
+ expect(isActionAllowed("auth_users", "create", perms)).toBe(false);
36
+ });
37
+
38
+ it("returns false when the collection's value is explicitly undefined", () => {
39
+ const perms = { articles: undefined };
40
+ expect(isActionAllowed("articles", "read", perms)).toBe(false);
41
+ expect(isActionAllowed("articles", "create", perms)).toBe(false);
42
+ });
43
+
44
+ it("returns false when the collection is an empty {}", () => {
45
+ const perms = { articles: {} };
46
+ expect(isActionAllowed("articles", "read", perms)).toBe(false);
47
+ expect(isActionAllowed("articles", "create", perms)).toBe(false);
48
+ expect(isActionAllowed("articles", "update", perms)).toBe(false);
49
+ expect(isActionAllowed("articles", "delete", perms)).toBe(false);
50
+ });
51
+
52
+ it("returns true when the collection is explicitly `true`", () => {
53
+ const perms = { articles: true as const };
54
+ expect(isActionAllowed("articles", "read", perms)).toBe(true);
55
+ expect(isActionAllowed("articles", "create", perms)).toBe(true);
56
+ expect(isActionAllowed("articles", "update", perms)).toBe(true);
57
+ expect(isActionAllowed("articles", "delete", perms)).toBe(true);
58
+ });
59
+ });
60
+
61
+ describe("action level", () => {
62
+ it("returns false when the action is missing from the collection", () => {
63
+ const perms = { articles: { read: true } };
64
+ expect(isActionAllowed("articles", "create", perms)).toBe(false);
65
+ expect(isActionAllowed("articles", "update", perms)).toBe(false);
66
+ expect(isActionAllowed("articles", "delete", perms)).toBe(false);
67
+ });
68
+
69
+ it("returns false when the action is an empty {} — no constraints listed", () => {
70
+ // This is the footgun the rule fixes: an empty constraint object used
71
+ // to count as "conditional grant with no constraints" → effectively
72
+ // unconditional allow. Now it collapses to no grant.
73
+ expect(isActionAllowed("articles", "read", { articles: { read: {} } })).toBe(false);
74
+ expect(isActionAllowed("articles", "create", { articles: { create: {} } })).toBe(false);
75
+ expect(isActionAllowed("articles", "update", { articles: { update: {} } })).toBe(false);
76
+ expect(isActionAllowed("articles", "delete", { articles: { delete: {} } })).toBe(false);
77
+ });
78
+
79
+ it("returns false when the action is explicitly undefined", () => {
80
+ const perms = { articles: { read: undefined, create: undefined } };
81
+ expect(isActionAllowed("articles", "read", perms)).toBe(false);
82
+ expect(isActionAllowed("articles", "create", perms)).toBe(false);
83
+ });
84
+
85
+ it("returns true when the action is explicitly `true`", () => {
86
+ expect(isActionAllowed("articles", "read", { articles: { read: true } })).toBe(true);
87
+ expect(isActionAllowed("articles", "create", { articles: { create: true } })).toBe(true);
88
+ });
89
+
90
+ it("returns true when the action has at least one real constraint", () => {
91
+ // `filter` for read
92
+ expect(
93
+ isActionAllowed("articles", "read", {
94
+ articles: { read: { filter: { id: 1 } } },
95
+ }),
96
+ ).toBe(true);
97
+ // `fields` allowlist for read
98
+ expect(
99
+ isActionAllowed("articles", "read", {
100
+ articles: { read: { fields: { title: true } } },
101
+ }),
102
+ ).toBe(true);
103
+ // `fields` allowlist for create
104
+ expect(
105
+ isActionAllowed("articles", "create", {
106
+ articles: { create: { fields: { title: true } } },
107
+ }),
108
+ ).toBe(true);
109
+ });
110
+ });
111
+
112
+ describe("cross-level: only the relevant level needs to grant", () => {
113
+ it("an empty {} at any single level kills the chain regardless of siblings", () => {
114
+ // Other collections have grants, but `articles` is `{}` → no articles access.
115
+ const perms = { auth_users: { read: true }, articles: {} };
116
+ expect(isActionAllowed("articles", "read", perms)).toBe(false);
117
+ // Sibling still works.
118
+ expect(isActionAllowed("auth_users", "read", perms)).toBe(true);
119
+ });
120
+
121
+ it("an empty {} action doesn't bleed into other actions on the same collection", () => {
122
+ const perms = { articles: { read: true, create: {} } };
123
+ expect(isActionAllowed("articles", "read", perms)).toBe(true);
124
+ expect(isActionAllowed("articles", "create", perms)).toBe(false);
125
+ });
126
+ });
127
+ });
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { isShareSubsetOfCreator } from "../../workflows/shareIntersection.ts";
3
+
4
+ describe("isShareSubsetOfCreator", () => {
5
+ describe("creator: true (admin)", () => {
6
+ it("allows any share, including true", () => {
7
+ expect(isShareSubsetOfCreator(true, true)).toBe(true);
8
+ expect(isShareSubsetOfCreator({ articles: { read: true } }, true)).toBe(true);
9
+ expect(
10
+ isShareSubsetOfCreator(
11
+ { auth_users: { read: true, update: true } },
12
+ true,
13
+ ),
14
+ ).toBe(true);
15
+ });
16
+ });
17
+
18
+ describe("share: true (unconditional)", () => {
19
+ it("requires creator: true — anything else is broader", () => {
20
+ expect(isShareSubsetOfCreator(true, { articles: { read: true } })).toBe(false);
21
+ expect(isShareSubsetOfCreator(true, undefined)).toBe(false);
22
+ expect(isShareSubsetOfCreator(true, {})).toBe(false);
23
+ });
24
+ });
25
+
26
+ describe("empty / missing share", () => {
27
+ it("an empty share is trivially a subset", () => {
28
+ expect(isShareSubsetOfCreator(undefined, undefined)).toBe(true);
29
+ expect(isShareSubsetOfCreator({}, undefined)).toBe(true);
30
+ expect(isShareSubsetOfCreator({}, { articles: { read: true } })).toBe(true);
31
+ });
32
+ });
33
+
34
+ describe("missing creator collection", () => {
35
+ it("rejects when share names a collection the creator has no perm for", () => {
36
+ expect(
37
+ isShareSubsetOfCreator(
38
+ { auth_users: { read: true } },
39
+ { articles: { read: true } },
40
+ ),
41
+ ).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe("creator: true on collection", () => {
46
+ it("allows any action grant on that collection", () => {
47
+ expect(
48
+ isShareSubsetOfCreator(
49
+ { articles: { read: true, create: true } },
50
+ { articles: true },
51
+ ),
52
+ ).toBe(true);
53
+ expect(
54
+ isShareSubsetOfCreator(
55
+ { articles: { read: { filter: { id: 1 } } } },
56
+ { articles: true },
57
+ ),
58
+ ).toBe(true);
59
+ });
60
+ });
61
+
62
+ describe("share: true on a collection where creator is conditional", () => {
63
+ it("rejects — share unconditional > creator conditional", () => {
64
+ expect(
65
+ isShareSubsetOfCreator(
66
+ { articles: true },
67
+ { articles: { read: true } },
68
+ ),
69
+ ).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe("share: true on an action where creator is conditional", () => {
74
+ it("rejects — unconditional action > conditional action", () => {
75
+ expect(
76
+ isShareSubsetOfCreator(
77
+ { articles: { read: true } },
78
+ { articles: { read: { filter: { user_id: 1 } } } },
79
+ ),
80
+ ).toBe(false);
81
+ });
82
+
83
+ it("accepts when creator is also true on that action", () => {
84
+ expect(
85
+ isShareSubsetOfCreator(
86
+ { articles: { read: true } },
87
+ { articles: { read: true } },
88
+ ),
89
+ ).toBe(true);
90
+ });
91
+ });
92
+
93
+ describe("missing creator action", () => {
94
+ it("rejects when share has an action the creator doesn't", () => {
95
+ expect(
96
+ isShareSubsetOfCreator(
97
+ { articles: { create: true } },
98
+ { articles: { read: true } },
99
+ ),
100
+ ).toBe(false);
101
+ });
102
+ });
103
+
104
+ describe("fields allowlist subsetting", () => {
105
+ it("accepts when share's fields are a subset of creator's", () => {
106
+ expect(
107
+ isShareSubsetOfCreator(
108
+ { articles: { create: { fields: { title: true } } } },
109
+ { articles: { create: { fields: { title: true, body: true } } } },
110
+ ),
111
+ ).toBe(true);
112
+ });
113
+
114
+ it("rejects when share's fields include one the creator doesn't allow", () => {
115
+ expect(
116
+ isShareSubsetOfCreator(
117
+ {
118
+ articles: {
119
+ create: { fields: { title: true, published: true } },
120
+ },
121
+ },
122
+ { articles: { create: { fields: { title: true, body: true } } } },
123
+ ),
124
+ ).toBe(false);
125
+ });
126
+
127
+ it("rejects share that omits fields restriction when creator has one", () => {
128
+ // No fields key = no restriction = broader than the creator.
129
+ expect(
130
+ isShareSubsetOfCreator(
131
+ { articles: { create: {} } },
132
+ { articles: { create: { fields: { title: true } } } },
133
+ ),
134
+ ).toBe(false);
135
+ });
136
+
137
+ it("allows omitting fields when creator has no fields restriction either", () => {
138
+ expect(
139
+ isShareSubsetOfCreator(
140
+ { articles: { create: {} } },
141
+ { articles: { create: {} as any } },
142
+ ),
143
+ ).toBe(true);
144
+ });
145
+ });
146
+
147
+ describe("filter subsetting (intentionally not enforced)", () => {
148
+ it("accepts any share filter when creator has a conditional read", () => {
149
+ // Documented limitation — we don't try to prove filter subsets.
150
+ expect(
151
+ isShareSubsetOfCreator(
152
+ { articles: { read: { filter: { id: 42 } } } },
153
+ { articles: { read: { filter: { user_id: 1 } } } },
154
+ ),
155
+ ).toBe(true);
156
+ });
157
+ });
158
+ });
@@ -39,45 +39,67 @@ export function getBaseWorkflows(_extensionConfig: ExtensionConfig): Workflow[]
39
39
  }
40
40
 
41
41
  export const baseWorkflows: Workflow[] = [
42
+ // TODO: we should never use this hook at all. we should totally remove it from the core
43
+ // and we should instead use the controller level to implement
44
+ // people can abuse this and start creating custom endpoints with it which should be discouraged totally
45
+ // and users shouldnt be allowed to do because they they loose all lobb features for nothing. they lose auth, logs, workflows etc...
42
46
  {
43
47
  name: "auth_BearerTokenHandler",
44
48
  eventName: "core.webserver.middlwares.pre",
45
49
  handler: async (input, ctx) => {
46
50
  const token = getBearerToken(input.context);
47
- if (token) {
48
- const sessionsResult = await ctx.workflows.collectionService({
51
+ if (!token) return input;
52
+
53
+ const context = input.context as Context;
54
+
55
+ // 1) Try the token as a normal user session.
56
+ const sessionsResult = await ctx.workflows.collectionService({
57
+ method: "findAll",
58
+ props: {
59
+ collectionName: "auth_sessions",
60
+ params: { filter: { token } },
61
+ },
62
+ });
63
+ const session = sessionsResult.data[0];
64
+
65
+ if (session) {
66
+ const usersResult = await ctx.workflows.collectionService({
49
67
  method: "findAll",
50
68
  props: {
51
- collectionName: "auth_sessions",
52
- params: {
53
- filter: {
54
- token,
55
- },
56
- },
69
+ collectionName: "auth_users",
70
+ params: { filter: { id: session.user_id } },
57
71
  },
58
72
  });
73
+ const user = usersResult.data[0];
59
74
 
60
- const session = sessionsResult.data[0];
61
-
62
- if (session) {
63
- const usersResult = await ctx.workflows.collectionService({
64
- method: "findAll",
65
- props: {
66
- collectionName: "auth_users",
67
- params: {
68
- filter: {
69
- id: session.user_id,
70
- },
71
- },
72
- },
73
- });
74
- const user = usersResult.data[0];
75
+ context.set("auth_session", session);
76
+ context.set("auth_user", user);
77
+ return input;
78
+ }
75
79
 
76
- const context = input.context as Context;
77
- context.set("auth_session", session);
78
- context.set("auth_user", user);
80
+ // 2) Otherwise try the token as a share. Shares carry their own
81
+ // permissions snapshot and have a fixed expiry — no user identity.
82
+ const sharesResult = await ctx.workflows.collectionService({
83
+ method: "findAll",
84
+ props: {
85
+ collectionName: "auth_shares",
86
+ params: { filter: { token } },
87
+ },
88
+ });
89
+ const share = sharesResult.data[0];
90
+
91
+ if (share && new Date(share.expires_at) > new Date()) {
92
+ let permissions: any = {};
93
+ try {
94
+ permissions = JSON.parse(share.permissions ?? "{}");
95
+ } catch {
96
+ // malformed permissions — treat as no access
97
+ permissions = {};
79
98
  }
99
+ context.set("auth_share", { ...share, permissions });
80
100
  }
101
+
102
+ return input;
81
103
  },
82
104
  },
83
105
  // auth_logins workflows
@@ -0,0 +1,32 @@
1
+ import type { Workflow } from "@lobb-js/core";
2
+ import type { Context } from "hono";
3
+ import type { ExtensionConfig } from "../config/extensionConfigSchema.ts";
4
+
5
+ // Augment the response of GET /api/collections/auth_users/me with the
6
+ // user's effective permissions, so the studio can gate UI without
7
+ // exposing the full roles map in meta. Other reads of auth_users
8
+ // (another user, or the current user fetching themselves by numeric id)
9
+ // are untouched. Sits at the controller layer so `permissions` is a
10
+ // sibling of `data` in the response wrapper, not mixed into the row.
11
+ export function getCurrentUserPermissionsWorkflow(
12
+ extensionConfig: ExtensionConfig,
13
+ ): Workflow {
14
+ return {
15
+ name: "auth_attachCurrentUserPermissions",
16
+ eventName: "core.controllers.findOne",
17
+ handler: async (input) => {
18
+ if (input.collectionName !== "auth_users") return input;
19
+ const context = input.context as Context | undefined;
20
+ if (context?.req.param("id") !== "me") return input;
21
+
22
+ const role = input.response?.data?.role;
23
+ // Strip functions (e.g. dynamic filter predicates) — they can't be
24
+ // serialised to the client.
25
+ const permissions = JSON.parse(JSON.stringify(
26
+ extensionConfig.roles?.[role]?.permissions ?? {},
27
+ ));
28
+ input.response = { ...input.response, permissions };
29
+ return input;
30
+ },
31
+ };
32
+ }
@@ -2,16 +2,28 @@ import type { Workflow } from "@lobb-js/core";
2
2
  import type { ExtensionConfig } from "../config/extensionConfigSchema.ts";
3
3
  import { getPoliciesWorkflows } from "./policiesWorkflows.ts";
4
4
  import { meAliasWorkflows } from "./meAliasWorkflows.ts";
5
+ import { getCurrentUserPermissionsWorkflow } from "./currentUserPermissionsWorkflow.ts";
5
6
  import { getBaseWorkflows } from "./baseWorkflow.ts";
7
+ import { getSharesWorkflows } from "./sharesWorkflows.ts";
8
+ import { init } from "../database/init.ts";
6
9
  export function getWorkflows(extensionConfig: ExtensionConfig): Workflow[] {
7
10
  return [
11
+ {
12
+ name: "auth_init",
13
+ eventName: "core.init",
14
+ handler: async (_input, ctx) => {
15
+ await init(ctx.lobb, extensionConfig);
16
+ },
17
+ },
8
18
  ...getBaseWorkflows(extensionConfig),
19
+ ...getSharesWorkflows(extensionConfig),
9
20
 
10
21
  // TODO: think about putting the below workflows above at the beggining
11
22
  // this will give us the ability to specify which roles have the ability to login, register etc
12
23
 
13
24
  // workflows that handles policies
14
25
  ...meAliasWorkflows,
26
+ getCurrentUserPermissionsWorkflow(extensionConfig),
15
27
  ...getPoliciesWorkflows(extensionConfig),
16
28
  // TODO: record users activity feed
17
29
  // {
@@ -25,6 +25,32 @@ export const meAliasWorkflows: Workflow[] = [
25
25
  name: "auth_userFindOneMeAlias",
26
26
  eventName: "core.store.preFindOne",
27
27
  handler: async (input, ctx) => {
28
+ // Share bearers don't have an auth_users row to fetch — short-circuit
29
+ // the findOne with the share's permissions snapshot. Studio uses the
30
+ // same /me endpoint to learn what the current bearer can do regardless
31
+ // of bearer type. Falls through to resolveMeAlias otherwise (which
32
+ // handles the session case and throws for unauthenticated requests).
33
+ if (input.collectionName === "auth_users" && input.id === "me") {
34
+ const context = input.context as Context;
35
+ const user = context.get("auth_user") as User | undefined;
36
+ if (!user) {
37
+ const share = context.get("auth_share") as
38
+ | { permissions: any }
39
+ | undefined;
40
+ if (share) {
41
+ throw new Response(
42
+ JSON.stringify({
43
+ data: null,
44
+ permissions: share.permissions,
45
+ }),
46
+ {
47
+ status: 200,
48
+ headers: { "Content-Type": "application/json" },
49
+ },
50
+ );
51
+ }
52
+ }
53
+ }
28
54
  input.id = resolveMeAlias(input, ctx);
29
55
  return input;
30
56
  },