s3db.js 13.5.1 → 13.6.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/README.md +25 -10
- package/dist/{s3db.cjs.js → s3db.cjs} +30323 -24958
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +24026 -18654
- package/dist/s3db.es.js.map +1 -1
- package/package.json +216 -20
- package/src/concerns/id.js +90 -6
- package/src/concerns/index.js +2 -1
- package/src/concerns/password-hashing.js +150 -0
- package/src/database.class.js +4 -0
- package/src/plugins/api/auth/basic-auth.js +23 -1
- package/src/plugins/api/auth/index.js +49 -3
- package/src/plugins/api/auth/oauth2-auth.js +171 -0
- package/src/plugins/api/auth/oidc-auth.js +789 -0
- package/src/plugins/api/auth/oidc-client.js +462 -0
- package/src/plugins/api/auth/path-auth-matcher.js +284 -0
- package/src/plugins/api/concerns/event-emitter.js +134 -0
- package/src/plugins/api/concerns/failban-manager.js +651 -0
- package/src/plugins/api/concerns/guards-helpers.js +402 -0
- package/src/plugins/api/concerns/metrics-collector.js +346 -0
- package/src/plugins/api/index.js +503 -54
- package/src/plugins/api/middlewares/failban.js +305 -0
- package/src/plugins/api/middlewares/rate-limit.js +301 -0
- package/src/plugins/api/middlewares/request-id.js +74 -0
- package/src/plugins/api/middlewares/security-headers.js +120 -0
- package/src/plugins/api/middlewares/session-tracking.js +194 -0
- package/src/plugins/api/routes/auth-routes.js +23 -3
- package/src/plugins/api/routes/resource-routes.js +71 -29
- package/src/plugins/api/server.js +1017 -94
- package/src/plugins/api/utils/guards.js +213 -0
- package/src/plugins/api/utils/mime-types.js +154 -0
- package/src/plugins/api/utils/openapi-generator.js +44 -11
- package/src/plugins/api/utils/path-matcher.js +173 -0
- package/src/plugins/api/utils/static-filesystem.js +262 -0
- package/src/plugins/api/utils/static-s3.js +231 -0
- package/src/plugins/api/utils/template-engine.js +188 -0
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
- package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
- package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
- package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
- package/src/plugins/cloud-inventory/index.js +20 -0
- package/src/plugins/cloud-inventory/registry.js +146 -0
- package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
- package/src/plugins/cloud-inventory.plugin.js +1333 -0
- package/src/plugins/concerns/plugin-dependencies.js +61 -1
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- package/src/plugins/identity/README.md +335 -0
- package/src/plugins/identity/concerns/mfa-manager.js +204 -0
- package/src/plugins/identity/concerns/password.js +138 -0
- package/src/plugins/identity/concerns/resource-schemas.js +273 -0
- package/src/plugins/identity/concerns/token-generator.js +172 -0
- package/src/plugins/identity/email-service.js +422 -0
- package/src/plugins/identity/index.js +1052 -0
- package/src/plugins/identity/oauth2-server.js +1033 -0
- package/src/plugins/identity/oidc-discovery.js +285 -0
- package/src/plugins/identity/rsa-keys.js +323 -0
- package/src/plugins/identity/server.js +500 -0
- package/src/plugins/identity/session-manager.js +453 -0
- package/src/plugins/identity/ui/layouts/base.js +251 -0
- package/src/plugins/identity/ui/middleware.js +135 -0
- package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
- package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
- package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
- package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
- package/src/plugins/identity/ui/pages/admin/users.js +263 -0
- package/src/plugins/identity/ui/pages/consent.js +262 -0
- package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
- package/src/plugins/identity/ui/pages/login.js +144 -0
- package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
- package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
- package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
- package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
- package/src/plugins/identity/ui/pages/profile.js +361 -0
- package/src/plugins/identity/ui/pages/register.js +226 -0
- package/src/plugins/identity/ui/pages/reset-password.js +128 -0
- package/src/plugins/identity/ui/pages/verify-email.js +172 -0
- package/src/plugins/identity/ui/routes.js +2541 -0
- package/src/plugins/identity/ui/styles/main.css +465 -0
- package/src/plugins/index.js +4 -1
- package/src/plugins/ml/base-model.class.js +32 -7
- package/src/plugins/ml/classification-model.class.js +1 -1
- package/src/plugins/ml/timeseries-model.class.js +3 -1
- package/src/plugins/ml.plugin.js +124 -32
- package/src/plugins/shared/error-handler.js +147 -0
- package/src/plugins/shared/index.js +9 -0
- package/src/plugins/shared/middlewares/compression.js +117 -0
- package/src/plugins/shared/middlewares/cors.js +49 -0
- package/src/plugins/shared/middlewares/index.js +11 -0
- package/src/plugins/shared/middlewares/logging.js +54 -0
- package/src/plugins/shared/middlewares/rate-limit.js +73 -0
- package/src/plugins/shared/middlewares/security.js +158 -0
- package/src/plugins/shared/response-formatter.js +264 -0
- package/src/resource.class.js +140 -12
- package/src/schema.class.js +30 -1
- package/src/validator.class.js +57 -6
- package/dist/s3db.cjs.js.map +0 -1
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards Helpers - Framework-agnostic context creation
|
|
3
|
+
*
|
|
4
|
+
* Creates GuardContext from different web frameworks (Express, Hono, Fastify)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create framework-agnostic GuardContext from Express request
|
|
9
|
+
* @param {Object} req - Express request
|
|
10
|
+
* @returns {Object} GuardContext
|
|
11
|
+
*/
|
|
12
|
+
export function createExpressContext(req) {
|
|
13
|
+
const context = {
|
|
14
|
+
user: req.user || {},
|
|
15
|
+
params: req.params || {},
|
|
16
|
+
body: req.body || {},
|
|
17
|
+
query: req.query || {},
|
|
18
|
+
headers: req.headers || {},
|
|
19
|
+
|
|
20
|
+
// Internal state
|
|
21
|
+
partitionName: null,
|
|
22
|
+
partitionValues: {},
|
|
23
|
+
tenantId: null,
|
|
24
|
+
userId: null,
|
|
25
|
+
|
|
26
|
+
// Helper to set partition
|
|
27
|
+
setPartition(name, values) {
|
|
28
|
+
this.partitionName = name;
|
|
29
|
+
this.partitionValues = values;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Framework raw (for advanced use)
|
|
33
|
+
raw: { req }
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return context;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create framework-agnostic GuardContext from Hono context
|
|
41
|
+
* @param {Object} c - Hono context
|
|
42
|
+
* @returns {Promise<Object>} GuardContext
|
|
43
|
+
*/
|
|
44
|
+
export async function createHonoContext(c) {
|
|
45
|
+
const context = {
|
|
46
|
+
user: c.get('user') || {},
|
|
47
|
+
params: c.req.param(),
|
|
48
|
+
body: await c.req.json().catch(() => ({})),
|
|
49
|
+
query: c.req.query(),
|
|
50
|
+
headers: Object.fromEntries(c.req.raw.headers.entries()),
|
|
51
|
+
|
|
52
|
+
// Internal state
|
|
53
|
+
partitionName: null,
|
|
54
|
+
partitionValues: {},
|
|
55
|
+
tenantId: null,
|
|
56
|
+
userId: null,
|
|
57
|
+
|
|
58
|
+
// Helper to set partition
|
|
59
|
+
setPartition(name, values) {
|
|
60
|
+
this.partitionName = name;
|
|
61
|
+
this.partitionValues = values;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// Framework raw
|
|
65
|
+
raw: { c }
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return context;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create framework-agnostic GuardContext from Fastify request
|
|
73
|
+
* @param {Object} request - Fastify request
|
|
74
|
+
* @returns {Object} GuardContext
|
|
75
|
+
*/
|
|
76
|
+
export function createFastifyContext(request) {
|
|
77
|
+
const context = {
|
|
78
|
+
user: request.user || {},
|
|
79
|
+
params: request.params || {},
|
|
80
|
+
body: request.body || {},
|
|
81
|
+
query: request.query || {},
|
|
82
|
+
headers: request.headers || {},
|
|
83
|
+
|
|
84
|
+
// Internal state
|
|
85
|
+
partitionName: null,
|
|
86
|
+
partitionValues: {},
|
|
87
|
+
tenantId: null,
|
|
88
|
+
userId: null,
|
|
89
|
+
|
|
90
|
+
// Helper to set partition
|
|
91
|
+
setPartition(name, values) {
|
|
92
|
+
this.partitionName = name;
|
|
93
|
+
this.partitionValues = values;
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// Framework raw
|
|
97
|
+
raw: { request }
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return context;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Execute guards and apply results to list options
|
|
105
|
+
* @param {Resource} resource - Resource instance
|
|
106
|
+
* @param {Object} context - GuardContext
|
|
107
|
+
* @param {Object} options - List options
|
|
108
|
+
* @returns {Promise<Object>} Modified options
|
|
109
|
+
*/
|
|
110
|
+
export async function applyGuardsToList(resource, context, options = {}) {
|
|
111
|
+
// Execute list guard
|
|
112
|
+
const allowed = await resource.executeGuard('list', context);
|
|
113
|
+
|
|
114
|
+
if (!allowed) {
|
|
115
|
+
throw new Error('Forbidden: Guard denied access to list');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Apply partition from guard if set
|
|
119
|
+
if (context.partitionName) {
|
|
120
|
+
options.partition = context.partitionName;
|
|
121
|
+
options.partitionValues = context.partitionValues || {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return options;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Execute guards for get operation
|
|
129
|
+
* @param {Resource} resource - Resource instance
|
|
130
|
+
* @param {Object} context - GuardContext
|
|
131
|
+
* @param {Object} record - Record to check
|
|
132
|
+
* @returns {Promise<Object|null>} Record if allowed, null if denied
|
|
133
|
+
*/
|
|
134
|
+
export async function applyGuardsToGet(resource, context, record) {
|
|
135
|
+
if (!record) return null;
|
|
136
|
+
|
|
137
|
+
// Execute get guard
|
|
138
|
+
const allowed = await resource.executeGuard('get', context, record);
|
|
139
|
+
|
|
140
|
+
if (!allowed) {
|
|
141
|
+
// Return null instead of error (404 instead of 403)
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return record;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Execute guards for insert operation
|
|
150
|
+
* @param {Resource} resource - Resource instance
|
|
151
|
+
* @param {Object} context - GuardContext
|
|
152
|
+
* @param {Object} data - Data to insert
|
|
153
|
+
* @returns {Promise<Object>} Modified data
|
|
154
|
+
*/
|
|
155
|
+
export async function applyGuardsToInsert(resource, context, data) {
|
|
156
|
+
// Execute insert guard
|
|
157
|
+
const allowed = await resource.executeGuard('insert', context);
|
|
158
|
+
|
|
159
|
+
if (!allowed) {
|
|
160
|
+
throw new Error('Forbidden: Guard denied access to insert');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Guard may have modified context.body (e.g., force tenantId/userId)
|
|
164
|
+
if (context.body && typeof context.body === 'object') {
|
|
165
|
+
// Merge guard modifications into data
|
|
166
|
+
return { ...data, ...context.body };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return data;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Execute guards for update operation
|
|
174
|
+
* @param {Resource} resource - Resource instance
|
|
175
|
+
* @param {Object} context - GuardContext
|
|
176
|
+
* @param {Object} record - Current record
|
|
177
|
+
* @returns {Promise<boolean>} True if allowed
|
|
178
|
+
*/
|
|
179
|
+
export async function applyGuardsToUpdate(resource, context, record) {
|
|
180
|
+
if (!record) {
|
|
181
|
+
throw new Error('Resource not found');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Execute update guard
|
|
185
|
+
const allowed = await resource.executeGuard('update', context, record);
|
|
186
|
+
|
|
187
|
+
if (!allowed) {
|
|
188
|
+
throw new Error('Forbidden: Guard denied access to update');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Execute guards for delete operation
|
|
196
|
+
* @param {Resource} resource - Resource instance
|
|
197
|
+
* @param {Object} context - GuardContext
|
|
198
|
+
* @param {Object} record - Record to delete
|
|
199
|
+
* @returns {Promise<boolean>} True if allowed
|
|
200
|
+
*/
|
|
201
|
+
export async function applyGuardsToDelete(resource, context, record) {
|
|
202
|
+
if (!record) {
|
|
203
|
+
throw new Error('Resource not found');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Execute delete guard
|
|
207
|
+
const allowed = await resource.executeGuard('delete', context, record);
|
|
208
|
+
|
|
209
|
+
if (!allowed) {
|
|
210
|
+
throw new Error('Forbidden: Guard denied access to delete');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if user has required scopes
|
|
218
|
+
*
|
|
219
|
+
* @param {Array<string>} requiredScopes - Required scopes
|
|
220
|
+
* @param {string} mode - 'any' or 'all' (default: 'any')
|
|
221
|
+
* @returns {Function} Guard function
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* // Require admin scope
|
|
225
|
+
* guard: {
|
|
226
|
+
* delete: requireScopes(['admin'])
|
|
227
|
+
* }
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* // Require ANY of multiple scopes
|
|
231
|
+
* guard: {
|
|
232
|
+
* update: requireScopes(['admin', 'moderator'], 'any')
|
|
233
|
+
* }
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* // Require ALL scopes
|
|
237
|
+
* guard: {
|
|
238
|
+
* create: requireScopes(['write:urls', 'verified'], 'all')
|
|
239
|
+
* }
|
|
240
|
+
*/
|
|
241
|
+
export function requireScopes(requiredScopes, mode = 'any') {
|
|
242
|
+
if (!Array.isArray(requiredScopes)) {
|
|
243
|
+
requiredScopes = [requiredScopes];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return (ctx) => {
|
|
247
|
+
const userScopes = ctx.user?.scopes || [];
|
|
248
|
+
|
|
249
|
+
if (mode === 'all') {
|
|
250
|
+
// User must have ALL required scopes
|
|
251
|
+
return requiredScopes.every(scope => userScopes.includes(scope));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// mode === 'any': User must have AT LEAST ONE required scope
|
|
255
|
+
return requiredScopes.some(scope => userScopes.includes(scope));
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if user has required role
|
|
261
|
+
*
|
|
262
|
+
* @param {string|Array<string>} role - Required role(s)
|
|
263
|
+
* @returns {Function} Guard function
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* guard: {
|
|
267
|
+
* delete: requireRole('admin')
|
|
268
|
+
* }
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* // Multiple roles (any)
|
|
272
|
+
* guard: {
|
|
273
|
+
* update: requireRole(['admin', 'moderator'])
|
|
274
|
+
* }
|
|
275
|
+
*/
|
|
276
|
+
export function requireRole(role) {
|
|
277
|
+
const roles = Array.isArray(role) ? role : [role];
|
|
278
|
+
|
|
279
|
+
return (ctx) => {
|
|
280
|
+
const userRole = ctx.user?.role;
|
|
281
|
+
const userRoles = ctx.user?.roles || [];
|
|
282
|
+
|
|
283
|
+
// Check single role field
|
|
284
|
+
if (userRole && roles.includes(userRole)) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Check roles array
|
|
289
|
+
return roles.some(r => userRoles.includes(r));
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Require admin scope (shorthand for requireScopes(['admin']))
|
|
295
|
+
*
|
|
296
|
+
* @returns {Function} Guard function
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* guard: {
|
|
300
|
+
* delete: requireAdmin()
|
|
301
|
+
* }
|
|
302
|
+
*/
|
|
303
|
+
export function requireAdmin() {
|
|
304
|
+
return requireScopes(['admin']);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Check ownership (record.userId === ctx.user.sub)
|
|
309
|
+
*
|
|
310
|
+
* @param {string} field - Field to check (default: 'userId')
|
|
311
|
+
* @returns {Function} Guard function
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* guard: {
|
|
315
|
+
* update: requireOwnership(),
|
|
316
|
+
* delete: requireOwnership('createdBy')
|
|
317
|
+
* }
|
|
318
|
+
*/
|
|
319
|
+
export function requireOwnership(field = 'userId') {
|
|
320
|
+
return (ctx, resource) => {
|
|
321
|
+
if (!resource) return false;
|
|
322
|
+
|
|
323
|
+
const userId = ctx.user?.sub || ctx.user?.id;
|
|
324
|
+
if (!userId) return false;
|
|
325
|
+
|
|
326
|
+
return resource[field] === userId;
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Combine guards with OR logic (any guard passes = allowed)
|
|
332
|
+
*
|
|
333
|
+
* @param {...Function} guards - Guard functions
|
|
334
|
+
* @returns {Function} Combined guard function
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* guard: {
|
|
338
|
+
* delete: anyOf(
|
|
339
|
+
* requireAdmin(),
|
|
340
|
+
* requireOwnership()
|
|
341
|
+
* )
|
|
342
|
+
* }
|
|
343
|
+
*/
|
|
344
|
+
export function anyOf(...guards) {
|
|
345
|
+
return async (ctx, resource) => {
|
|
346
|
+
for (const guard of guards) {
|
|
347
|
+
const result = await guard(ctx, resource);
|
|
348
|
+
if (result) return true;
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Combine guards with AND logic (all guards must pass)
|
|
356
|
+
*
|
|
357
|
+
* @param {...Function} guards - Guard functions
|
|
358
|
+
* @returns {Function} Combined guard function
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* guard: {
|
|
362
|
+
* create: allOf(
|
|
363
|
+
* requireScopes(['write:urls']),
|
|
364
|
+
* (ctx) => ctx.user.verified === true
|
|
365
|
+
* )
|
|
366
|
+
* }
|
|
367
|
+
*/
|
|
368
|
+
export function allOf(...guards) {
|
|
369
|
+
return async (ctx, resource) => {
|
|
370
|
+
for (const guard of guards) {
|
|
371
|
+
const result = await guard(ctx, resource);
|
|
372
|
+
if (!result) return false;
|
|
373
|
+
}
|
|
374
|
+
return true;
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Check if user belongs to specific tenant
|
|
380
|
+
*
|
|
381
|
+
* @param {string} tenantField - Field name in resource (default: 'tenantId')
|
|
382
|
+
* @returns {Function} Guard function
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* guard: {
|
|
386
|
+
* '*': (ctx) => {
|
|
387
|
+
* ctx.tenantId = ctx.user.tenantId || ctx.user.tid;
|
|
388
|
+
* return !!ctx.tenantId;
|
|
389
|
+
* },
|
|
390
|
+
* update: requireTenant()
|
|
391
|
+
* }
|
|
392
|
+
*/
|
|
393
|
+
export function requireTenant(tenantField = 'tenantId') {
|
|
394
|
+
return (ctx, resource) => {
|
|
395
|
+
if (!resource) return true; // Let wildcard/insert guards handle
|
|
396
|
+
|
|
397
|
+
const userTenantId = ctx.tenantId || ctx.user?.tenantId || ctx.user?.tid;
|
|
398
|
+
if (!userTenantId) return false;
|
|
399
|
+
|
|
400
|
+
return resource[tenantField] === userTenantId;
|
|
401
|
+
};
|
|
402
|
+
}
|