@lobb-js/lobb-ext-auth 0.11.0 → 0.11.2
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/README.md +1 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +23 -4
- package/dist/index.js +41 -2
- package/dist/lib/components/pages/settings/index.svelte +1 -1
- package/dist/lib/components/pages/settings/pages/activityFeed.svelte +1 -1
- package/dist/lib/components/pages/settings/pages/rolesAndPermissions.svelte +1 -1
- package/dist/lib/components/pages/settings/pages/users.svelte +1 -1
- package/dist/lib/components/pages/userSettings/index.svelte +45 -32
- package/dist/onStartup.js +17 -2
- package/dist/shared/permissions.d.ts +2 -0
- package/dist/shared/permissions.js +35 -0
- package/extensions/auth/collections/collections.ts +2 -0
- package/extensions/auth/collections/shares.ts +60 -0
- package/extensions/auth/config/extensionConfigSchema.d.ts +41 -0
- package/extensions/auth/config/permissionsAction/create.d.ts +18 -0
- package/extensions/auth/config/permissionsAction/delete.d.ts +3 -0
- package/extensions/auth/config/permissionsAction/read.d.ts +11 -0
- package/extensions/auth/config/permissionsAction/update.d.ts +18 -0
- package/extensions/auth/index.ts +0 -2
- package/extensions/auth/studio/auth.ts +25 -5
- package/extensions/auth/studio/index.ts +44 -2
- package/extensions/auth/studio/lib/components/pages/settings/index.svelte +1 -1
- package/extensions/auth/studio/lib/components/pages/settings/pages/activityFeed.svelte +1 -1
- package/extensions/auth/studio/lib/components/pages/settings/pages/rolesAndPermissions.svelte +1 -1
- package/extensions/auth/studio/lib/components/pages/settings/pages/users.svelte +1 -1
- package/extensions/auth/studio/lib/components/pages/userSettings/index.svelte +45 -32
- package/extensions/auth/studio/onStartup.ts +14 -2
- package/extensions/auth/studio/shared/permissions.ts +42 -0
- package/extensions/auth/tests/collections/shares.test.ts +657 -0
- package/extensions/auth/tests/configs/auth.ts +17 -0
- package/extensions/auth/tests/controllers/me.test.ts +104 -0
- package/extensions/auth/tests/permissions.test.ts +127 -0
- package/extensions/auth/tests/workflows/shareIntersection.test.ts +158 -0
- package/extensions/auth/workflows/baseWorkflow.ts +48 -26
- package/extensions/auth/workflows/currentUserPermissionsWorkflow.ts +32 -0
- package/extensions/auth/workflows/index.ts +12 -0
- package/extensions/auth/workflows/meAliasWorkflows.ts +26 -0
- package/extensions/auth/workflows/policiesWorkflows.ts +64 -117
- package/extensions/auth/workflows/shareIntersection.ts +64 -0
- package/extensions/auth/workflows/sharesWorkflows.ts +135 -0
- package/extensions/auth/workflows/utils.ts +132 -224
- package/package.json +3 -3
- package/dist/lib/components/pages/userSettings/components/account.svelte +0 -106
- package/dist/lib/components/pages/userSettings/components/account.svelte.d.ts +0 -14
- package/dist/lib/components/pages/userSettings/components/profile.svelte +0 -87
- package/dist/lib/components/pages/userSettings/components/profile.svelte.d.ts +0 -14
- package/extensions/auth/studio/lib/components/pages/userSettings/components/account.svelte +0 -106
- package/extensions/auth/studio/lib/components/pages/userSettings/components/profile.svelte +0 -87
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { Lobb, Workflow } from "@lobb-js/core";
|
|
2
2
|
import type { Context } from "hono";
|
|
3
|
-
import type { ExtensionConfig
|
|
4
|
-
import {
|
|
3
|
+
import type { ExtensionConfig } from "../config/extensionConfigSchema.ts";
|
|
4
|
+
import {
|
|
5
|
+
handlePolicy,
|
|
6
|
+
handleReadFields,
|
|
7
|
+
resolveRequestPermissions,
|
|
8
|
+
} from "./utils.ts";
|
|
5
9
|
|
|
6
10
|
export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow[] {
|
|
7
11
|
return [
|
|
@@ -12,22 +16,22 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
12
16
|
if (input.triggeredBy === "API") {
|
|
13
17
|
const context = input.context as Context;
|
|
14
18
|
const lobb = context.get("lobb") as Lobb;
|
|
15
|
-
const user =
|
|
19
|
+
const { permissions, user } = resolveRequestPermissions(
|
|
20
|
+
context,
|
|
21
|
+
extensionConfig,
|
|
22
|
+
ctx,
|
|
23
|
+
);
|
|
16
24
|
|
|
17
25
|
const output = handlePolicy({
|
|
18
|
-
action: "create",
|
|
19
26
|
collectionName: input.collectionName,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
input,
|
|
27
|
+
action: "create",
|
|
28
|
+
permissions,
|
|
23
29
|
lobb,
|
|
24
30
|
ctx,
|
|
25
|
-
|
|
31
|
+
user,
|
|
32
|
+
payload: input.data,
|
|
26
33
|
});
|
|
27
|
-
|
|
28
|
-
if (output && output.payload) {
|
|
29
|
-
input.data = output.payload;
|
|
30
|
-
}
|
|
34
|
+
if (output?.payload) input.data = output.payload;
|
|
31
35
|
}
|
|
32
36
|
return input;
|
|
33
37
|
},
|
|
@@ -39,55 +43,22 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
39
43
|
if (input.triggeredBy === "API") {
|
|
40
44
|
const context = input.context as Context;
|
|
41
45
|
const lobb = context.get("lobb") as Lobb;
|
|
42
|
-
const user =
|
|
43
|
-
|
|
44
|
-
const output = handlePolicy({
|
|
45
|
-
action: "read",
|
|
46
|
-
collectionName: input.collectionName,
|
|
47
|
-
role: user?.role,
|
|
48
|
-
user,
|
|
49
|
-
input,
|
|
50
|
-
lobb,
|
|
51
|
-
ctx,
|
|
46
|
+
const { permissions, user } = resolveRequestPermissions(
|
|
47
|
+
context,
|
|
52
48
|
extensionConfig,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (output && output.filter) {
|
|
56
|
-
input.filter = output.filter;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return input;
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: "auth_policyPreFindOne",
|
|
64
|
-
eventName: "core.store.preFindOne",
|
|
65
|
-
handler: async (input, ctx) => {
|
|
66
|
-
if (input.triggeredBy === "API") {
|
|
67
|
-
const context = input.context as Context;
|
|
68
|
-
const lobb = context.get("lobb") as Lobb;
|
|
69
|
-
const user = context.get("auth_user") as User | undefined;
|
|
70
|
-
|
|
71
|
-
// pass if the user is effecting himself
|
|
72
|
-
const currentUser = input.id === user?.id;
|
|
73
|
-
if (input.collectionName === "auth_users" && currentUser) {
|
|
74
|
-
return input;
|
|
75
|
-
}
|
|
49
|
+
ctx,
|
|
50
|
+
);
|
|
76
51
|
|
|
77
52
|
const output = handlePolicy({
|
|
78
|
-
action: "read",
|
|
79
53
|
collectionName: input.collectionName,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
input,
|
|
54
|
+
action: "read",
|
|
55
|
+
permissions,
|
|
83
56
|
lobb,
|
|
84
57
|
ctx,
|
|
85
|
-
|
|
58
|
+
user,
|
|
59
|
+
filter: input.params?.filter,
|
|
86
60
|
});
|
|
87
|
-
|
|
88
|
-
if (output && output.filter) {
|
|
89
|
-
input.filter = output.filter;
|
|
90
|
-
}
|
|
61
|
+
if (output?.filter) input.filter = output.filter;
|
|
91
62
|
}
|
|
92
63
|
return input;
|
|
93
64
|
},
|
|
@@ -99,24 +70,23 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
99
70
|
if (input.triggeredBy === "API") {
|
|
100
71
|
const context = input.context as Context;
|
|
101
72
|
const lobb = context.get("lobb") as Lobb;
|
|
102
|
-
const user =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return input;
|
|
108
|
-
}
|
|
73
|
+
const { permissions, user } = resolveRequestPermissions(
|
|
74
|
+
context,
|
|
75
|
+
extensionConfig,
|
|
76
|
+
ctx,
|
|
77
|
+
);
|
|
109
78
|
|
|
110
|
-
handlePolicy({
|
|
111
|
-
action: "update",
|
|
79
|
+
const output = handlePolicy({
|
|
112
80
|
collectionName: input.collectionName,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
input,
|
|
81
|
+
action: "update",
|
|
82
|
+
permissions,
|
|
116
83
|
lobb,
|
|
117
84
|
ctx,
|
|
118
|
-
|
|
85
|
+
user,
|
|
86
|
+
payload: input.data,
|
|
87
|
+
recordId: input.id,
|
|
119
88
|
});
|
|
89
|
+
if (output?.payload) input.data = output.payload;
|
|
120
90
|
}
|
|
121
91
|
return input;
|
|
122
92
|
},
|
|
@@ -128,42 +98,39 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
128
98
|
if (input.triggeredBy === "API") {
|
|
129
99
|
const context = input.context as Context;
|
|
130
100
|
const lobb = context.get("lobb") as Lobb;
|
|
131
|
-
const user =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return input;
|
|
137
|
-
}
|
|
101
|
+
const { permissions, user } = resolveRequestPermissions(
|
|
102
|
+
context,
|
|
103
|
+
extensionConfig,
|
|
104
|
+
ctx,
|
|
105
|
+
);
|
|
138
106
|
|
|
139
107
|
handlePolicy({
|
|
140
|
-
action: "delete",
|
|
141
108
|
collectionName: input.collectionName,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
input,
|
|
109
|
+
action: "delete",
|
|
110
|
+
permissions,
|
|
145
111
|
lobb,
|
|
146
112
|
ctx,
|
|
147
|
-
|
|
113
|
+
user,
|
|
114
|
+
recordId: input.id,
|
|
148
115
|
});
|
|
149
116
|
}
|
|
150
117
|
return input;
|
|
151
118
|
},
|
|
152
119
|
},
|
|
153
|
-
//
|
|
120
|
+
// Post-fetch field-level filtering using the role/share's `read.fields`
|
|
121
|
+
// allowlist (static — works the same for both sources).
|
|
154
122
|
{
|
|
155
123
|
name: "auth_policyPostQuery",
|
|
156
124
|
eventName: "core.store.findAll",
|
|
157
125
|
handler: async (input, ctx) => {
|
|
158
126
|
if (input.triggeredBy === "API") {
|
|
159
127
|
const context = input.context as Context;
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
user?.role,
|
|
163
|
-
input.collectionName,
|
|
164
|
-
input.data,
|
|
128
|
+
const { permissions } = resolveRequestPermissions(
|
|
129
|
+
context,
|
|
165
130
|
extensionConfig,
|
|
131
|
+
ctx,
|
|
166
132
|
);
|
|
133
|
+
input.data = handleReadFields(permissions, input.collectionName, input.data);
|
|
167
134
|
}
|
|
168
135
|
return input;
|
|
169
136
|
},
|
|
@@ -174,13 +141,12 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
174
141
|
handler: async (input, ctx) => {
|
|
175
142
|
if (input.triggeredBy === "API") {
|
|
176
143
|
const context = input.context as Context;
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
user?.role,
|
|
180
|
-
input.collectionName,
|
|
181
|
-
input.data,
|
|
144
|
+
const { permissions } = resolveRequestPermissions(
|
|
145
|
+
context,
|
|
182
146
|
extensionConfig,
|
|
147
|
+
ctx,
|
|
183
148
|
);
|
|
149
|
+
input.data = handleReadFields(permissions, input.collectionName, input.data);
|
|
184
150
|
}
|
|
185
151
|
return input;
|
|
186
152
|
},
|
|
@@ -191,13 +157,12 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
191
157
|
handler: async (input, ctx) => {
|
|
192
158
|
if (input.triggeredBy === "API") {
|
|
193
159
|
const context = input.context as Context;
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
user?.role,
|
|
197
|
-
input.collectionName,
|
|
198
|
-
input.data,
|
|
160
|
+
const { permissions } = resolveRequestPermissions(
|
|
161
|
+
context,
|
|
199
162
|
extensionConfig,
|
|
163
|
+
ctx,
|
|
200
164
|
);
|
|
165
|
+
input.data = handleReadFields(permissions, input.collectionName, input.data);
|
|
201
166
|
}
|
|
202
167
|
return input;
|
|
203
168
|
},
|
|
@@ -208,30 +173,12 @@ export function getPoliciesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
208
173
|
handler: async (input, ctx) => {
|
|
209
174
|
if (input.triggeredBy === "API") {
|
|
210
175
|
const context = input.context as Context;
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
user?.role,
|
|
214
|
-
input.collectionName,
|
|
215
|
-
input.data,
|
|
216
|
-
extensionConfig,
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
return input;
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
name: "auth_policyPostReadForDelete",
|
|
224
|
-
eventName: "core.store.updateOne",
|
|
225
|
-
handler: async (input, ctx) => {
|
|
226
|
-
if (input.triggeredBy === "API") {
|
|
227
|
-
const context = input.context as Context;
|
|
228
|
-
const user = context.get("auth_user") as User | undefined;
|
|
229
|
-
input.data = handleReadFields(
|
|
230
|
-
user?.role,
|
|
231
|
-
input.collectionName,
|
|
232
|
-
input.data,
|
|
176
|
+
const { permissions } = resolveRequestPermissions(
|
|
177
|
+
context,
|
|
233
178
|
extensionConfig,
|
|
179
|
+
ctx,
|
|
234
180
|
);
|
|
181
|
+
input.data = handleReadFields(permissions, input.collectionName, input.data);
|
|
235
182
|
}
|
|
236
183
|
return input;
|
|
237
184
|
},
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CollectionPermissionsConfig,
|
|
3
|
+
PermissionsConfig,
|
|
4
|
+
} from "../config/extensionConfigSchema.ts";
|
|
5
|
+
|
|
6
|
+
// Returns true if `share` permissions are fully covered by `creator`
|
|
7
|
+
// permissions — i.e. a user with `creator` is allowed to mint a share that
|
|
8
|
+
// grants `share`. Used to enforce that share creators can never grant more
|
|
9
|
+
// than they have themselves.
|
|
10
|
+
//
|
|
11
|
+
// Filter comparison is intentionally not structural: when both sides are
|
|
12
|
+
// conditional reads with their own filter, the share filter is accepted
|
|
13
|
+
// as-is. Statically deciding whether one filter is a subset of another is
|
|
14
|
+
// intractable in the general case; the documented behaviour is that the
|
|
15
|
+
// share's filter applies as written to share-token requests.
|
|
16
|
+
export function isShareSubsetOfCreator(
|
|
17
|
+
share: PermissionsConfig | undefined,
|
|
18
|
+
creator: PermissionsConfig | undefined,
|
|
19
|
+
): boolean {
|
|
20
|
+
if (creator === true) return true;
|
|
21
|
+
if (share === true) return false;
|
|
22
|
+
if (!share) return true;
|
|
23
|
+
|
|
24
|
+
for (const [collection, sharePerm] of Object.entries(share)) {
|
|
25
|
+
if (sharePerm === undefined) continue;
|
|
26
|
+
if (!isCollectionSubset(sharePerm, creator?.[collection])) return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isCollectionSubset(
|
|
32
|
+
share: CollectionPermissionsConfig,
|
|
33
|
+
creator: CollectionPermissionsConfig | undefined,
|
|
34
|
+
): boolean {
|
|
35
|
+
if (creator === true) return true;
|
|
36
|
+
if (!creator) return false;
|
|
37
|
+
if (share === true) return false;
|
|
38
|
+
|
|
39
|
+
for (const [action, sharePerm] of Object.entries(share)) {
|
|
40
|
+
if (sharePerm === undefined) continue;
|
|
41
|
+
const creatorPerm = (creator as Record<string, unknown>)[action];
|
|
42
|
+
if (!isActionSubset(sharePerm, creatorPerm)) return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isActionSubset(share: unknown, creator: unknown): boolean {
|
|
48
|
+
if (creator === true) return true;
|
|
49
|
+
if (creator === undefined || creator === null) return false;
|
|
50
|
+
if (share === true) return false;
|
|
51
|
+
|
|
52
|
+
// Both conditional — verify the share's fields allowlist is a subset of
|
|
53
|
+
// the creator's (if the creator has one).
|
|
54
|
+
const creatorObj = creator as { fields?: Record<string, true> };
|
|
55
|
+
const shareObj = share as { fields?: Record<string, true> };
|
|
56
|
+
if (creatorObj.fields) {
|
|
57
|
+
if (!shareObj.fields) return false;
|
|
58
|
+
const creatorFields = Object.keys(creatorObj.fields);
|
|
59
|
+
for (const f of Object.keys(shareObj.fields)) {
|
|
60
|
+
if (!creatorFields.includes(f)) return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { Workflow } from "@lobb-js/core";
|
|
2
|
+
import type { Context } from "hono";
|
|
3
|
+
import type {
|
|
4
|
+
ExtensionConfig,
|
|
5
|
+
PermissionsConfig,
|
|
6
|
+
User,
|
|
7
|
+
} from "../config/extensionConfigSchema.ts";
|
|
8
|
+
import { generateRandomId } from "../utils.ts";
|
|
9
|
+
import { resolveRequestPermissions } from "./utils.ts";
|
|
10
|
+
import { isShareSubsetOfCreator } from "./shareIntersection.ts";
|
|
11
|
+
|
|
12
|
+
export function getSharesWorkflows(extensionConfig: ExtensionConfig): Workflow[] {
|
|
13
|
+
return [
|
|
14
|
+
// Server-side generates the share's bearer token on create so callers
|
|
15
|
+
// can't pick a guessable value. The freshly generated token is returned
|
|
16
|
+
// in the createOne response (since we mutate input.data before the row
|
|
17
|
+
// is written).
|
|
18
|
+
{
|
|
19
|
+
name: "auth_generateShareToken",
|
|
20
|
+
eventName: "core.store.preCreateOne",
|
|
21
|
+
handler: async (input) => {
|
|
22
|
+
if (input.collectionName === "auth_shares") {
|
|
23
|
+
input.data.token = generateRandomId();
|
|
24
|
+
}
|
|
25
|
+
return input;
|
|
26
|
+
},
|
|
27
|
+
},
|
|
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.
|
|
35
|
+
{
|
|
36
|
+
name: "auth_normalizeShareExpiry",
|
|
37
|
+
eventName: "core.service.preCreateOne",
|
|
38
|
+
handler: async (input, ctx) => {
|
|
39
|
+
if (input.collectionName !== "auth_shares") return input;
|
|
40
|
+
|
|
41
|
+
const seconds = input.data.expires_in_seconds;
|
|
42
|
+
if (typeof seconds === "number") {
|
|
43
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
44
|
+
throw new ctx.LobbError({
|
|
45
|
+
code: "BAD_REQUEST",
|
|
46
|
+
message: "expires_in_seconds must be a positive number.",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
input.data.expires_at = new Date(Date.now() + seconds * 1000)
|
|
50
|
+
.toISOString();
|
|
51
|
+
delete input.data.expires_in_seconds;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!input.data.expires_at) {
|
|
55
|
+
throw new ctx.LobbError({
|
|
56
|
+
code: "BAD_REQUEST",
|
|
57
|
+
message:
|
|
58
|
+
"Must provide either expires_at or expires_in_seconds when creating a share.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return input;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
// Auto-set created_by from the authenticated request user so callers
|
|
66
|
+
// can't forge the field. Internal callers (tests/server code using the
|
|
67
|
+
// service layer directly) are trusted and must supply created_by
|
|
68
|
+
// themselves.
|
|
69
|
+
{
|
|
70
|
+
name: "auth_setShareCreatedBy",
|
|
71
|
+
eventName: "core.store.preCreateOne",
|
|
72
|
+
handler: async (input) => {
|
|
73
|
+
if (input.collectionName !== "auth_shares") return input;
|
|
74
|
+
if (input.triggeredBy !== "API") return input;
|
|
75
|
+
const context = input.context as Context;
|
|
76
|
+
const user = context.get("auth_user") as User | undefined;
|
|
77
|
+
if (user) input.data.created_by = user.id;
|
|
78
|
+
return input;
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
// Intersection guard: a share's embedded permissions snapshot must be a
|
|
82
|
+
// subset of the creator's own permissions. Without this, any user who can
|
|
83
|
+
// create rows in auth_shares could mint a token that grants admin-level
|
|
84
|
+
// access. Only runs for API-triggered creates — internal callers (server
|
|
85
|
+
// code, tests using collectionService directly) are trusted.
|
|
86
|
+
{
|
|
87
|
+
name: "auth_validateShareSubset",
|
|
88
|
+
eventName: "core.store.preCreateOne",
|
|
89
|
+
handler: async (input, ctx) => {
|
|
90
|
+
if (input.collectionName !== "auth_shares") return input;
|
|
91
|
+
if (input.triggeredBy !== "API") return input;
|
|
92
|
+
|
|
93
|
+
const context = input.context as Context;
|
|
94
|
+
const { permissions: creatorPermissions } = resolveRequestPermissions(
|
|
95
|
+
context,
|
|
96
|
+
extensionConfig,
|
|
97
|
+
ctx,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
let sharePermissions: PermissionsConfig;
|
|
101
|
+
try {
|
|
102
|
+
sharePermissions = JSON.parse(input.data.permissions);
|
|
103
|
+
} catch {
|
|
104
|
+
throw new ctx.LobbError({
|
|
105
|
+
code: "BAD_REQUEST",
|
|
106
|
+
message: "Share permissions must be valid JSON.",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!isShareSubsetOfCreator(sharePermissions, creatorPermissions)) {
|
|
111
|
+
throw new ctx.LobbError({
|
|
112
|
+
code: "FORBIDDEN",
|
|
113
|
+
message:
|
|
114
|
+
"Cannot create a share with broader permissions than your own.",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return input;
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
// Hourly housekeeping: delete shares whose expires_at is in the past.
|
|
122
|
+
// Expired shares are already rejected at auth time, so this is just to
|
|
123
|
+
// keep the table from growing unbounded.
|
|
124
|
+
{
|
|
125
|
+
name: "auth_cleanupExpiredShares",
|
|
126
|
+
eventName: "0 3 * * *",
|
|
127
|
+
handler: async (_input, ctx) => {
|
|
128
|
+
await ctx.lobb.collectionService.deleteMany({
|
|
129
|
+
collectionName: "auth_shares",
|
|
130
|
+
filter: { expires_at: { $lt: new Date().toISOString() } },
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
}
|