@aooth/arbac-moost 0.1.76 → 0.1.78
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/atscript/index.d.mts +61 -3
- package/dist/atscript/index.mjs +82 -4
- package/dist/index.d.mts +17 -0
- package/dist/index.mjs +303 -20
- package/package.json +20 -20
|
@@ -32,6 +32,61 @@ interface ArbacUserTable<T extends object> {
|
|
|
32
32
|
};
|
|
33
33
|
}): Promise<T | null>;
|
|
34
34
|
}
|
|
35
|
+
/** Options of {@link AtscriptArbacUserProvider}. @since 0.1.78 */
|
|
36
|
+
interface AtscriptArbacUserProviderOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Extra user fields to select in the provider's single per-event user
|
|
39
|
+
* fetch, next to the id, `@arbac.role` and `@arbac.attribute` fields — read
|
|
40
|
+
* them in an `extractAttrs` / `extractRoles` override instead of fetching
|
|
41
|
+
* the user a second time (e.g. a `username` that scope functions need but
|
|
42
|
+
* that cannot carry `@arbac.attribute`).
|
|
43
|
+
*/
|
|
44
|
+
extraSelect?: readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* OPT-IN cross-request cache of the fetched user record (roles + attrs),
|
|
47
|
+
* see {@link ArbacUserRecordCache}. Off by default: without it every
|
|
48
|
+
* request reads the user once.
|
|
49
|
+
*/
|
|
50
|
+
cache?: ArbacUserRecordCache;
|
|
51
|
+
}
|
|
52
|
+
/** Options of {@link ArbacUserRecordCache}. @since 0.1.78 */
|
|
53
|
+
interface ArbacUserRecordCacheOptions {
|
|
54
|
+
/** How long a fetched record is reused, in ms (keep it short — seconds). */
|
|
55
|
+
ttlMs: number;
|
|
56
|
+
/** Most cached users; the oldest entry is dropped past it. Default 1000. */
|
|
57
|
+
max?: number;
|
|
58
|
+
/** Clock (ms) — for tests. Default `Date.now`. */
|
|
59
|
+
now?: () => number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* OPT-IN, process-local cache of the user records an
|
|
63
|
+
* {@link AtscriptArbacUserProvider} fetches for ARBAC (`userId` → roles +
|
|
64
|
+
* attributes), shared across requests for `ttlMs`. Pass it as the provider's
|
|
65
|
+
* `cache` option; off by default.
|
|
66
|
+
*
|
|
67
|
+
* Staleness: a role or attribute change reaches ARBAC within `ttlMs` — or
|
|
68
|
+
* immediately on THIS process when you call {@link invalidate} (wire it to
|
|
69
|
+
* your user writes, e.g. `userService.onUserWrite((id) => cache.invalidate(id))`).
|
|
70
|
+
* Other processes keep their entry until it expires, so the cross-node lag is
|
|
71
|
+
* at most `ttlMs`. Only the stored record is cached: credential attenuation
|
|
72
|
+
* (read from the token per request) and every policy evaluation still run
|
|
73
|
+
* per request. A missing user is never cached.
|
|
74
|
+
*
|
|
75
|
+
* @since 0.1.78
|
|
76
|
+
*/
|
|
77
|
+
declare class ArbacUserRecordCache {
|
|
78
|
+
private readonly ttlMs;
|
|
79
|
+
private readonly max;
|
|
80
|
+
private readonly now;
|
|
81
|
+
private readonly entries;
|
|
82
|
+
constructor(opts: ArbacUserRecordCacheOptions);
|
|
83
|
+
/** Drop the cached record of `userId` (all users when omitted). */
|
|
84
|
+
invalidate(userId?: string): void;
|
|
85
|
+
/** @internal The live record cached for `owner` (a provider), if any. */
|
|
86
|
+
get(owner: object, userId: string): Promise<unknown> | undefined;
|
|
87
|
+
/** @internal Cache `record` (dropped again if it rejects or resolves to `null`). */
|
|
88
|
+
set(owner: object, userId: string, record: Promise<unknown>): void;
|
|
89
|
+
}
|
|
35
90
|
/**
|
|
36
91
|
* `@Injectable()` (SINGLETON) — moost@0.6.x's `Injectable` metadata is NOT
|
|
37
92
|
* inherited from the base `ArbacUserProvider` by infact, so the decorator
|
|
@@ -44,7 +99,9 @@ interface ArbacUserTable<T extends object> {
|
|
|
44
99
|
* table, and register the subclass via `setReplaceRegistry`.
|
|
45
100
|
*
|
|
46
101
|
* `extractRoles` and `extractAttrs` are protected seams — override to
|
|
47
|
-
* reshape roles/attrs without re-implementing the fetch path
|
|
102
|
+
* reshape roles/attrs without re-implementing the fetch path; pass
|
|
103
|
+
* `extraSelect` (third constructor argument) to have the fetch include the
|
|
104
|
+
* extra fields they read.
|
|
48
105
|
*/
|
|
49
106
|
declare abstract class AtscriptArbacUserProvider<T extends object = object> extends ArbacUserProvider {
|
|
50
107
|
protected readonly userType: TAtscriptAnnotatedType;
|
|
@@ -52,7 +109,8 @@ declare abstract class AtscriptArbacUserProvider<T extends object = object> exte
|
|
|
52
109
|
private readonly spec;
|
|
53
110
|
private readonly projection;
|
|
54
111
|
private readonly withClause;
|
|
55
|
-
|
|
112
|
+
private readonly recordCache;
|
|
113
|
+
constructor(userType: TAtscriptAnnotatedType, table: ArbacUserTable<T>, options?: AtscriptArbacUserProviderOptions);
|
|
56
114
|
abstract getUserId(): string | Promise<string>;
|
|
57
115
|
getRoles(id: string): Promise<string[]>;
|
|
58
116
|
getAttrs(id: string): Promise<object>;
|
|
@@ -233,4 +291,4 @@ declare class AoothArbacUserCredentials extends AoothUserCredentials {
|
|
|
233
291
|
}
|
|
234
292
|
// prettier-ignore-end
|
|
235
293
|
//#endregion
|
|
236
|
-
export { AoothArbacUserCredentials, type AoothCredentialMetadataSpec, type AoothUserHandleSpec, type ArbacAttenuationSpec, type ArbacUserTable, AtscriptArbacUserProvider, extractAttenuation, getAoothCredentialMetadataSpec, getAoothUserHandleSpec, getArbacAttenuationSpec, validateAttenuationTargets };
|
|
294
|
+
export { AoothArbacUserCredentials, type AoothCredentialMetadataSpec, type AoothUserHandleSpec, type ArbacAttenuationSpec, ArbacUserRecordCache, type ArbacUserRecordCacheOptions, type ArbacUserTable, AtscriptArbacUserProvider, type AtscriptArbacUserProviderOptions, extractAttenuation, getAoothCredentialMetadataSpec, getAoothUserHandleSpec, getArbacAttenuationSpec, validateAttenuationTargets };
|
package/dist/atscript/index.mjs
CHANGED
|
@@ -141,20 +141,88 @@ const useFetchCache = defineWook((ctx) => {
|
|
|
141
141
|
}
|
|
142
142
|
return cache;
|
|
143
143
|
});
|
|
144
|
+
/**
|
|
145
|
+
* OPT-IN, process-local cache of the user records an
|
|
146
|
+
* {@link AtscriptArbacUserProvider} fetches for ARBAC (`userId` → roles +
|
|
147
|
+
* attributes), shared across requests for `ttlMs`. Pass it as the provider's
|
|
148
|
+
* `cache` option; off by default.
|
|
149
|
+
*
|
|
150
|
+
* Staleness: a role or attribute change reaches ARBAC within `ttlMs` — or
|
|
151
|
+
* immediately on THIS process when you call {@link invalidate} (wire it to
|
|
152
|
+
* your user writes, e.g. `userService.onUserWrite((id) => cache.invalidate(id))`).
|
|
153
|
+
* Other processes keep their entry until it expires, so the cross-node lag is
|
|
154
|
+
* at most `ttlMs`. Only the stored record is cached: credential attenuation
|
|
155
|
+
* (read from the token per request) and every policy evaluation still run
|
|
156
|
+
* per request. A missing user is never cached.
|
|
157
|
+
*
|
|
158
|
+
* @since 0.1.78
|
|
159
|
+
*/
|
|
160
|
+
var ArbacUserRecordCache = class {
|
|
161
|
+
ttlMs;
|
|
162
|
+
max;
|
|
163
|
+
now;
|
|
164
|
+
entries = /* @__PURE__ */ new Map();
|
|
165
|
+
constructor(opts) {
|
|
166
|
+
if (!(opts.ttlMs > 0)) throw new Error("ArbacUserRecordCache: ttlMs must be > 0");
|
|
167
|
+
this.ttlMs = opts.ttlMs;
|
|
168
|
+
this.max = opts.max ?? 1e3;
|
|
169
|
+
this.now = opts.now ?? Date.now;
|
|
170
|
+
}
|
|
171
|
+
/** Drop the cached record of `userId` (all users when omitted). */
|
|
172
|
+
invalidate(userId) {
|
|
173
|
+
if (userId === void 0) this.entries.clear();
|
|
174
|
+
else this.entries.delete(userId);
|
|
175
|
+
}
|
|
176
|
+
/** @internal The live record cached for `owner` (a provider), if any. */
|
|
177
|
+
get(owner, userId) {
|
|
178
|
+
const entry = this.entries.get(userId);
|
|
179
|
+
if (!entry) return void 0;
|
|
180
|
+
if (entry.owner !== owner || entry.expiresAt <= this.now()) {
|
|
181
|
+
this.entries.delete(userId);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
return entry.record;
|
|
185
|
+
}
|
|
186
|
+
/** @internal Cache `record` (dropped again if it rejects or resolves to `null`). */
|
|
187
|
+
set(owner, userId, record) {
|
|
188
|
+
this.entries.delete(userId);
|
|
189
|
+
if (this.entries.size >= this.max) {
|
|
190
|
+
const oldest = this.entries.keys().next();
|
|
191
|
+
if (!oldest.done) this.entries.delete(oldest.value);
|
|
192
|
+
}
|
|
193
|
+
const entry = {
|
|
194
|
+
owner,
|
|
195
|
+
expiresAt: this.now() + this.ttlMs,
|
|
196
|
+
record
|
|
197
|
+
};
|
|
198
|
+
this.entries.set(userId, entry);
|
|
199
|
+
const drop = () => {
|
|
200
|
+
if (this.entries.get(userId) === entry) this.entries.delete(userId);
|
|
201
|
+
};
|
|
202
|
+
record.then((r) => r === null && drop(), drop);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
144
205
|
let AtscriptArbacUserProvider = class AtscriptArbacUserProvider extends ArbacUserProvider {
|
|
145
206
|
userType;
|
|
146
207
|
table;
|
|
147
208
|
spec;
|
|
148
209
|
projection;
|
|
149
210
|
withClause;
|
|
150
|
-
|
|
211
|
+
recordCache;
|
|
212
|
+
constructor(userType, table, options) {
|
|
151
213
|
super();
|
|
152
214
|
this.userType = userType;
|
|
153
215
|
this.table = table;
|
|
154
216
|
this.spec = getArbacExtractSpec(userType);
|
|
155
217
|
if (!this.spec.userIdField) throw new Error("AtscriptArbacUserProvider: userType has no @arbac.userId, @db.table.preferredId.uniqueIndex field, or @meta.id — annotate one to identify users");
|
|
156
|
-
|
|
218
|
+
const projection = getArbacProjection(userType);
|
|
219
|
+
const extra = options?.extraSelect?.filter((f) => !Object.hasOwn(projection, f)) ?? [];
|
|
220
|
+
this.projection = extra.length > 0 ? {
|
|
221
|
+
...projection,
|
|
222
|
+
...Object.fromEntries(extra.map((f) => [f, 1]))
|
|
223
|
+
} : projection;
|
|
157
224
|
this.withClause = this.spec.roleField?.shape === "rel.from" ? [{ name: this.spec.roleField.name }] : void 0;
|
|
225
|
+
this.recordCache = options?.cache;
|
|
158
226
|
}
|
|
159
227
|
async getRoles(id) {
|
|
160
228
|
const record = await this.fetchRecord(id);
|
|
@@ -217,6 +285,11 @@ let AtscriptArbacUserProvider = class AtscriptArbacUserProvider extends ArbacUse
|
|
|
217
285
|
}
|
|
218
286
|
const cached = perProvider.get(userId);
|
|
219
287
|
if (cached !== void 0) return cached;
|
|
288
|
+
const shared = this.recordCache?.get(this, userId);
|
|
289
|
+
if (shared !== void 0) {
|
|
290
|
+
perProvider.set(userId, shared);
|
|
291
|
+
return shared;
|
|
292
|
+
}
|
|
220
293
|
const userIdField = this.spec.userIdField;
|
|
221
294
|
const controls = { $select: this.projection };
|
|
222
295
|
if (this.withClause) controls.$with = this.withClause;
|
|
@@ -226,10 +299,15 @@ let AtscriptArbacUserProvider = class AtscriptArbacUserProvider extends ArbacUse
|
|
|
226
299
|
});
|
|
227
300
|
promise.catch(() => perProvider?.delete(userId));
|
|
228
301
|
perProvider.set(userId, promise);
|
|
302
|
+
this.recordCache?.set(this, userId, promise);
|
|
229
303
|
return promise;
|
|
230
304
|
}
|
|
231
305
|
};
|
|
232
|
-
AtscriptArbacUserProvider = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [
|
|
306
|
+
AtscriptArbacUserProvider = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [
|
|
307
|
+
Object,
|
|
308
|
+
Object,
|
|
309
|
+
Object
|
|
310
|
+
])], AtscriptArbacUserProvider);
|
|
233
311
|
//#endregion
|
|
234
312
|
//#region src/atscript/attenuation-extract.ts
|
|
235
313
|
const specCache$2 = /* @__PURE__ */ new WeakMap();
|
|
@@ -439,4 +517,4 @@ var AoothArbacUserCredentials = class {
|
|
|
439
517
|
};
|
|
440
518
|
defineAnnotatedType("object", AoothArbacUserCredentials).prop("id", defineAnnotatedType().designType("string").tags("string").annotate("meta.id", true).annotate("db.default.uuid", true).$type).prop("username", defineAnnotatedType().designType("string").tags("string").annotate("db.index.unique", "username_idx", true).$type).prop("version", defineAnnotatedType().designType("number").tags("int", "number").annotate("db.column.version", true).annotate("expect.int", true).$type).prop("password", defineAnnotatedType("object").prop("hash", defineAnnotatedType().designType("string").tags("string").$type).prop("history", defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).prop("lastChanged", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("isInitial", defineAnnotatedType().designType("boolean").tags("boolean").$type).annotate("db.patch.strategy", "merge").$type).prop("account", defineAnnotatedType("object").prop("active", defineAnnotatedType().designType("boolean").tags("boolean").$type).prop("locked", defineAnnotatedType().designType("boolean").tags("boolean").$type).prop("lockReason", defineAnnotatedType().designType("string").tags("string").$type).prop("lockEnds", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("failedLoginAttempts", defineAnnotatedType().designType("number").tags("number").$type).prop("lastLogin", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("pendingInvitation", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type).prop("verifiedEmail", defineAnnotatedType().designType("string").tags("string").optional().$type).annotate("db.patch.strategy", "merge").$type).prop("mfa", defineAnnotatedType("object").prop("methods", defineAnnotatedType("array").of(defineAnnotatedType("object").prop("name", defineAnnotatedType().designType("string").tags("string").$type).prop("confirmed", defineAnnotatedType().designType("boolean").tags("boolean").$type).prop("value", defineAnnotatedType().designType("string").tags("string").$type).prop("lastUsedWindow", defineAnnotatedType().designType("number").tags("int", "number").annotate("expect.int", true).optional().$type).$type).$type).prop("defaultMethod", defineAnnotatedType().designType("string").tags("string").$type).prop("autoSend", defineAnnotatedType().designType("boolean").tags("boolean").$type).annotate("db.patch.strategy", "merge").$type).prop("trustedDevices", defineAnnotatedType("array").of(defineAnnotatedType("object").prop("token", defineAnnotatedType().designType("string").tags("string").$type).prop("ip", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("issuedAt", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("expiresAt", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("name", defineAnnotatedType().designType("string").tags("string").optional().$type).$type).annotate("db.patch.strategy", "merge").optional().$type).prop("seenDevices", defineAnnotatedType("array").of(defineAnnotatedType("object").prop("token", defineAnnotatedType().designType("string").tags("string").$type).prop("ip", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("issuedAt", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("expiresAt", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("name", defineAnnotatedType().designType("string").tags("string").optional().$type).$type).annotate("db.patch.strategy", "merge").optional().$type).prop("roles", defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).annotate("arbac.role", true).$type);
|
|
441
519
|
//#endregion
|
|
442
|
-
export { AoothArbacUserCredentials, AtscriptArbacUserProvider, extractAttenuation, getAoothCredentialMetadataSpec, getAoothUserHandleSpec, getArbacAttenuationSpec, validateAttenuationTargets };
|
|
520
|
+
export { AoothArbacUserCredentials, ArbacUserRecordCache, AtscriptArbacUserProvider, extractAttenuation, getAoothCredentialMetadataSpec, getAoothUserHandleSpec, getArbacAttenuationSpec, validateAttenuationTargets };
|
package/dist/index.d.mts
CHANGED
|
@@ -884,6 +884,23 @@ declare class MoostArbac<TUserAttrs extends object, TScope extends object> exten
|
|
|
884
884
|
}): Promise<TArbacEvalResult$1<TScope>>;
|
|
885
885
|
private readonly warnedFields;
|
|
886
886
|
private readonly warnUnknownField;
|
|
887
|
+
private metaMemoMax;
|
|
888
|
+
/**
|
|
889
|
+
* Configure the `/meta` projection memo of the ARBAC DB / value-help
|
|
890
|
+
* controllers. The overlay still evaluates every grant per request; only
|
|
891
|
+
* the pure pruning of the cached envelope by those decisions is reused —
|
|
892
|
+
* keyed by the decisions themselves (never by user, scopes' JSON or row
|
|
893
|
+
* filters), up to `max` projections per envelope (default 64, LRU).
|
|
894
|
+
* Memoized projections are frozen and pre-serialized with a weak ETag.
|
|
895
|
+
* `false` turns it off (every `/meta` is pruned afresh).
|
|
896
|
+
*
|
|
897
|
+
* @since 0.1.78
|
|
898
|
+
*/
|
|
899
|
+
setMetaMemo(options: false | {
|
|
900
|
+
max?: number;
|
|
901
|
+
}): this;
|
|
902
|
+
/** Most memoized `/meta` projections per envelope (`0` = memo off). @since 0.1.78 */
|
|
903
|
+
getMetaMemoMax(): number;
|
|
887
904
|
/** The registered custom scope field rules. @since 0.1.72 */
|
|
888
905
|
getScopeFields(): TScopeFieldRules<TScope>;
|
|
889
906
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { Inherit, Injectable, TInterceptorPriority, defineBeforeInterceptor, get
|
|
|
7
7
|
import { computeInsights, containsRelationFilter, findAncestorInSet, getPath, hasRelationOp, isPlainObject, isRelationOp, translateQueryTree } from "@atscript/db";
|
|
8
8
|
import { AsDbController, AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, DB_CRUD_HANDLERS, VALUE_HELP_CRUD_HANDLERS, badRequest, discoverRowLevelActions, getControllerFormType, getDbEndpoint, hasActionDelegations, resolveTerminalRef } from "@atscript/moost-db";
|
|
9
9
|
import { groupByFields, walkFilter } from "@uniqu/core";
|
|
10
|
+
import { prerenderJson } from "@wooksjs/event-http";
|
|
10
11
|
//#region src/db/helpers.ts
|
|
11
12
|
/** `map.get(key)`, created with `create()` and stored on a miss. */
|
|
12
13
|
function getOrCreate(map, key, create) {
|
|
@@ -248,7 +249,7 @@ function buildScopeVisibility(scopes, table, opts = {}) {
|
|
|
248
249
|
return source === void 0 || alwaysVisible.has(source) || base(source);
|
|
249
250
|
};
|
|
250
251
|
const declared = /* @__PURE__ */ new Map();
|
|
251
|
-
return {
|
|
252
|
+
return tagBuilt({
|
|
252
253
|
allowed,
|
|
253
254
|
alwaysVisible,
|
|
254
255
|
withGrants,
|
|
@@ -261,7 +262,16 @@ function buildScopeVisibility(scopes, table, opts = {}) {
|
|
|
261
262
|
if (resolved || !withGrants.has(name)) return resolved ?? void 0;
|
|
262
263
|
return getOrCreate(declared, name, () => buildScopeVisibility(eff.withScopes(name), table?.relatedTable?.(name)));
|
|
263
264
|
}
|
|
264
|
-
};
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const BUILT = Symbol("arbac.builtVisibility");
|
|
268
|
+
function tagBuilt(vis) {
|
|
269
|
+
vis[BUILT] = vis;
|
|
270
|
+
return vis;
|
|
271
|
+
}
|
|
272
|
+
/** `vis` came from {@link buildScopeVisibility} as is (not hand-built, not a copy). */
|
|
273
|
+
function isBuiltVisibility(vis) {
|
|
274
|
+
return vis[BUILT] === vis;
|
|
265
275
|
}
|
|
266
276
|
const scopeVisibilityCache = /* @__PURE__ */ new WeakMap();
|
|
267
277
|
const NO_TABLE = {
|
|
@@ -329,6 +339,13 @@ function registeredControllers(target) {
|
|
|
329
339
|
return perTarget ? [...perTarget.values()] : [];
|
|
330
340
|
}
|
|
331
341
|
/**
|
|
342
|
+
* Whether an ARBAC DB / value-help controller of the current app serves
|
|
343
|
+
* `type` — stable per-app registration, unlike runtime metadata.
|
|
344
|
+
*/
|
|
345
|
+
function isArbacServedType(type) {
|
|
346
|
+
return registeredControllers(type).length > 0;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
332
349
|
* The ARBAC resource + action a controller's handler is authorized as — the
|
|
333
350
|
* precedence `useArbac()` applies while that handler serves a request.
|
|
334
351
|
* Resource: method `@ArbacResource` → class `@ArbacResource` → controller id
|
|
@@ -718,6 +735,8 @@ function fieldChildrenOf(table) {
|
|
|
718
735
|
}
|
|
719
736
|
//#endregion
|
|
720
737
|
//#region src/moost-arbac.ts
|
|
738
|
+
/** Default number of memoized `/meta` projections per envelope. */
|
|
739
|
+
const DEFAULT_META_MEMO_MAX = 64;
|
|
721
740
|
let MoostArbac = class MoostArbac extends Arbac$1 {
|
|
722
741
|
scopeFieldRules = {};
|
|
723
742
|
/**
|
|
@@ -783,6 +802,26 @@ let MoostArbac = class MoostArbac extends Arbac$1 {
|
|
|
783
802
|
this.warnedFields.add(field);
|
|
784
803
|
console.warn(`[arbac] Scope field "${field}" has no rule registered with registerScopeFields: it reaches only your own code, and an attenuated request carrying it fails with 500.`);
|
|
785
804
|
};
|
|
805
|
+
metaMemoMax = DEFAULT_META_MEMO_MAX;
|
|
806
|
+
/**
|
|
807
|
+
* Configure the `/meta` projection memo of the ARBAC DB / value-help
|
|
808
|
+
* controllers. The overlay still evaluates every grant per request; only
|
|
809
|
+
* the pure pruning of the cached envelope by those decisions is reused —
|
|
810
|
+
* keyed by the decisions themselves (never by user, scopes' JSON or row
|
|
811
|
+
* filters), up to `max` projections per envelope (default 64, LRU).
|
|
812
|
+
* Memoized projections are frozen and pre-serialized with a weak ETag.
|
|
813
|
+
* `false` turns it off (every `/meta` is pruned afresh).
|
|
814
|
+
*
|
|
815
|
+
* @since 0.1.78
|
|
816
|
+
*/
|
|
817
|
+
setMetaMemo(options) {
|
|
818
|
+
this.metaMemoMax = options === false ? 0 : Math.max(0, options.max ?? DEFAULT_META_MEMO_MAX);
|
|
819
|
+
return this;
|
|
820
|
+
}
|
|
821
|
+
/** Most memoized `/meta` projections per envelope (`0` = memo off). @since 0.1.78 */
|
|
822
|
+
getMetaMemoMax() {
|
|
823
|
+
return this.metaMemoMax;
|
|
824
|
+
}
|
|
786
825
|
/** The registered custom scope field rules. @since 0.1.72 */
|
|
787
826
|
getScopeFields() {
|
|
788
827
|
return this.scopeFieldRules;
|
|
@@ -836,6 +875,7 @@ async function loadPrincipal(user) {
|
|
|
836
875
|
attrs: (id) => attrs ??= Promise.resolve(user.getAttrs(id))
|
|
837
876
|
};
|
|
838
877
|
}
|
|
878
|
+
const arbacDeps = cachedBy((_controller, ctx) => Promise.all([useControllerContext(ctx).instantiate(ArbacUserProviderToken), useControllerContext(ctx).instantiate(MoostArbac)]));
|
|
839
879
|
const principalKey = key("arbac.principal");
|
|
840
880
|
function isHttpEvent(ctx) {
|
|
841
881
|
return ctx.has(eventTypeKey) && ctx.get(eventTypeKey) === "http";
|
|
@@ -851,7 +891,7 @@ function evaluateArbac(ctx, target, memoPrincipal = false) {
|
|
|
851
891
|
const cc = useControllerContext(ctx);
|
|
852
892
|
const table = target.table ?? cc.getController()?.readable;
|
|
853
893
|
const pending = (async () => {
|
|
854
|
-
const [user, arbac] = await
|
|
894
|
+
const [user, arbac] = await arbacDeps(cc.getController(), ctx);
|
|
855
895
|
let principal;
|
|
856
896
|
if (!memoPrincipal || !isHttpEvent(ctx)) principal = loadPrincipal(user);
|
|
857
897
|
else if (ctx.has(principalKey)) principal = ctx.get(principalKey);
|
|
@@ -912,7 +952,12 @@ function conjoinForTable(a, b, table, fields) {
|
|
|
912
952
|
fields
|
|
913
953
|
})[0]);
|
|
914
954
|
}
|
|
915
|
-
const scopeFieldsSlot = cached(async (ctx) => (await useControllerContext(ctx).
|
|
955
|
+
const scopeFieldsSlot = cached(async (ctx) => (await arbacDeps(useControllerContext(ctx).getController(), ctx))[1].getScopeFields());
|
|
956
|
+
/** The current controller's `MoostArbac` — resolved once per (event, controller). */
|
|
957
|
+
async function currentMoostArbac() {
|
|
958
|
+
const ctx = current();
|
|
959
|
+
return (await arbacDeps(useControllerContext(ctx).getController(), ctx))[1];
|
|
960
|
+
}
|
|
916
961
|
/** The current app's custom scope field rules — resolved once per event. */
|
|
917
962
|
function currentScopeFields() {
|
|
918
963
|
return current().get(scopeFieldsSlot);
|
|
@@ -1083,6 +1128,189 @@ const ArbacResource = (name) => getArbacMate().decorate("arbacResourceId", name)
|
|
|
1083
1128
|
*/
|
|
1084
1129
|
const ArbacAction = (name) => getArbacMate().decorate("arbacActionId", name);
|
|
1085
1130
|
//#endregion
|
|
1131
|
+
//#region src/db/meta-memo.ts
|
|
1132
|
+
/** Deepest relation nesting the decision record follows; deeper → not memoized. */
|
|
1133
|
+
const MAX_RECORD_DEPTH = 16;
|
|
1134
|
+
var Unkeyable = class extends Error {};
|
|
1135
|
+
const UNKEYABLE = new Unkeyable("unkeyable /meta decision");
|
|
1136
|
+
function refuse() {
|
|
1137
|
+
throw UNKEYABLE;
|
|
1138
|
+
}
|
|
1139
|
+
const ids = /* @__PURE__ */ new WeakMap();
|
|
1140
|
+
let nextId = 1;
|
|
1141
|
+
function idOf(obj) {
|
|
1142
|
+
if (!obj) return 0;
|
|
1143
|
+
let id = ids.get(obj);
|
|
1144
|
+
if (id === void 0) ids.set(obj, id = nextId++);
|
|
1145
|
+
return id;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Validate a scope list without copying it: no symbol-keyed scope (the
|
|
1149
|
+
* `INHERITED_CONJUNCTION` marker of a one-sided `with` sub-scope) and no
|
|
1150
|
+
* function-valued scope piece, `with` sub-scopes recursively. Each scope
|
|
1151
|
+
* object is visited once per record (`seen`). The record itself keys on the
|
|
1152
|
+
* visibility DERIVED from the scopes (row data never reaches `/meta`).
|
|
1153
|
+
*/
|
|
1154
|
+
function checkScopes(scopes, seen, depth = 0) {
|
|
1155
|
+
if (!scopes) return;
|
|
1156
|
+
if (depth > MAX_RECORD_DEPTH) refuse();
|
|
1157
|
+
for (const scope of scopes) {
|
|
1158
|
+
if (seen.has(scope)) continue;
|
|
1159
|
+
if (scope === null || typeof scope !== "object") refuse();
|
|
1160
|
+
seen.add(scope);
|
|
1161
|
+
if (Object.getOwnPropertySymbols(scope).length > 0) refuse();
|
|
1162
|
+
const record = scope;
|
|
1163
|
+
for (const key of Object.keys(record)) {
|
|
1164
|
+
const value = record[key];
|
|
1165
|
+
if (typeof value === "function" || typeof value === "symbol") refuse();
|
|
1166
|
+
if (key === "with" && value !== void 0 && value !== null) {
|
|
1167
|
+
if (typeof value !== "object" || Object.getOwnPropertySymbols(value).length > 0) refuse();
|
|
1168
|
+
checkScopes(Object.values(value), seen, depth + 1);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
/** A projection (`path` → `0 | 1`) as sorted `[path, flag, …]`; anything else refuses. */
|
|
1174
|
+
function projectionKey(projection) {
|
|
1175
|
+
if (Object.getOwnPropertySymbols(projection).length > 0) refuse();
|
|
1176
|
+
const keys = Object.keys(projection).toSorted();
|
|
1177
|
+
const out = [];
|
|
1178
|
+
for (const key of keys) {
|
|
1179
|
+
const flag = projection[key];
|
|
1180
|
+
if (flag !== 0 && flag !== 1) refuse();
|
|
1181
|
+
out.push(key, flag);
|
|
1182
|
+
}
|
|
1183
|
+
return out;
|
|
1184
|
+
}
|
|
1185
|
+
/** Sorted strings of a set / list; a non-string refuses. */
|
|
1186
|
+
function sortedNames(names) {
|
|
1187
|
+
const out = [];
|
|
1188
|
+
for (const name of names) {
|
|
1189
|
+
if (typeof name !== "string") refuse();
|
|
1190
|
+
out.push(name);
|
|
1191
|
+
}
|
|
1192
|
+
return out.toSorted();
|
|
1193
|
+
}
|
|
1194
|
+
const NO_TREE = /* @__PURE__ */ new Map();
|
|
1195
|
+
/**
|
|
1196
|
+
* One visibility level: its table, normalized projection, parent-authority
|
|
1197
|
+
* relations, and every relation the pruning can resolve at this level — the
|
|
1198
|
+
* `/meta` relation names (`tree`, what the overlay resolved) and the declared
|
|
1199
|
+
* `with` names — as the child level or `null`. Only levels built by
|
|
1200
|
+
* `buildScopeVisibility` (whose `isAllowed` / `relation` are pure functions
|
|
1201
|
+
* of exactly these) are accepted.
|
|
1202
|
+
*/
|
|
1203
|
+
function encodeLevel(vis, tree, ctx, seen, depth) {
|
|
1204
|
+
if (depth > MAX_RECORD_DEPTH || !isBuiltVisibility(vis)) refuse();
|
|
1205
|
+
if (vis.alwaysVisible !== ctx.identifiers(vis.table)) refuse();
|
|
1206
|
+
checkScopes(vis.scopes, seen);
|
|
1207
|
+
const withGrants = sortedNames(vis.withGrants);
|
|
1208
|
+
const names = tree.size === 0 ? withGrants : sortedNames(/* @__PURE__ */ new Set([...tree.keys(), ...withGrants]));
|
|
1209
|
+
const relations = [];
|
|
1210
|
+
for (const name of names) {
|
|
1211
|
+
const sub = vis.relation?.(name);
|
|
1212
|
+
relations.push(name, sub ? encodeLevel(sub, tree.get(name) ?? NO_TREE, ctx, seen, depth + 1) : 0);
|
|
1213
|
+
}
|
|
1214
|
+
return [
|
|
1215
|
+
idOf(vis.table),
|
|
1216
|
+
projectionKey(vis.allowed),
|
|
1217
|
+
withGrants,
|
|
1218
|
+
relations
|
|
1219
|
+
];
|
|
1220
|
+
}
|
|
1221
|
+
/** Bit string of `flags`. */
|
|
1222
|
+
function bits(flags) {
|
|
1223
|
+
let out = "";
|
|
1224
|
+
for (const flag of flags) out += flag ? "1" : "0";
|
|
1225
|
+
return out;
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* The memo key of a `/meta` projection: its decision record, serialized —
|
|
1229
|
+
* or `undefined` when an input cannot be keyed exactly (then the request is
|
|
1230
|
+
* not memoized).
|
|
1231
|
+
*/
|
|
1232
|
+
function metaDecisionKey(input, ctx) {
|
|
1233
|
+
try {
|
|
1234
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1235
|
+
checkScopes(input.writeScopes, seen);
|
|
1236
|
+
checkScopes(input.readScopes, seen);
|
|
1237
|
+
const p = input.projection;
|
|
1238
|
+
if (!p) return JSON.stringify([bits(input.actions), bits(input.crud)]);
|
|
1239
|
+
const refTargets = [];
|
|
1240
|
+
const types = [...p.refTargets.keys()].toSorted((a, b) => idOf(a) - idOf(b));
|
|
1241
|
+
for (const type of types) {
|
|
1242
|
+
const vis = p.refTargets.get(type);
|
|
1243
|
+
refTargets.push(idOf(type), ctx.isServed(type), vis ? encodeLevel(vis, NO_TREE, ctx, seen, 1) : 0);
|
|
1244
|
+
}
|
|
1245
|
+
const { writable, nestedWrites, groupBy } = p;
|
|
1246
|
+
if (groupBy !== void 0 && typeof groupBy !== "boolean" && !Array.isArray(groupBy)) refuse();
|
|
1247
|
+
return JSON.stringify([
|
|
1248
|
+
bits(input.actions),
|
|
1249
|
+
bits(input.crud),
|
|
1250
|
+
encodeLevel(p.vis, p.tree, ctx, seen, 0),
|
|
1251
|
+
writable === void 0 ? 0 : writable === "all" ? 1 : sortedNames(writable),
|
|
1252
|
+
nestedWrites ? sortedNames(nestedWrites) : 0,
|
|
1253
|
+
Array.isArray(groupBy) ? sortedNames(groupBy) : groupBy ?? 0,
|
|
1254
|
+
refTargets,
|
|
1255
|
+
p.vis.table?.type ? ctx.isServed(p.vis.table.type) : 0
|
|
1256
|
+
]);
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
if (error instanceof Unkeyable) return void 0;
|
|
1259
|
+
throw error;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
const variants = /* @__PURE__ */ new WeakMap();
|
|
1263
|
+
const seenEnvelopes = /* @__PURE__ */ new WeakSet();
|
|
1264
|
+
/**
|
|
1265
|
+
* The projection of `envelope` for `key`, computed once per (envelope, key)
|
|
1266
|
+
* and reused while it stays among the `max` most recently used. No `key`
|
|
1267
|
+
* (unkeyable decisions) or `max` ≤ 0 → computed per call, uncached.
|
|
1268
|
+
*/
|
|
1269
|
+
function memoMetaProjection(envelope, key, max, compute) {
|
|
1270
|
+
if (key === void 0 || max <= 0) return compute();
|
|
1271
|
+
if (!seenEnvelopes.has(envelope)) {
|
|
1272
|
+
seenEnvelopes.add(envelope);
|
|
1273
|
+
return compute();
|
|
1274
|
+
}
|
|
1275
|
+
let byKey = variants.get(envelope);
|
|
1276
|
+
const hit = byKey?.get(key);
|
|
1277
|
+
if (hit) {
|
|
1278
|
+
byKey.delete(key);
|
|
1279
|
+
byKey.set(key, hit);
|
|
1280
|
+
return hit;
|
|
1281
|
+
}
|
|
1282
|
+
const out = shareMeta(compute());
|
|
1283
|
+
if (!byKey) variants.set(envelope, byKey = /* @__PURE__ */ new Map());
|
|
1284
|
+
while (byKey.size >= max) byKey.delete(byKey.keys().next().value);
|
|
1285
|
+
byKey.set(key, out);
|
|
1286
|
+
return out;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Make a memoized `/meta` object shareable across requests: frozen (deeply in
|
|
1290
|
+
* development / test, so an in-place mutation throws instead of serving old
|
|
1291
|
+
* bytes) and registered for prerendering with a weak ETag.
|
|
1292
|
+
*/
|
|
1293
|
+
function shareMeta(obj) {
|
|
1294
|
+
if (shouldDeepFreeze()) deepFreeze(obj);
|
|
1295
|
+
else Object.freeze(obj);
|
|
1296
|
+
return prerenderJson(obj, { etag: true });
|
|
1297
|
+
}
|
|
1298
|
+
let deepFreezeOn;
|
|
1299
|
+
function shouldDeepFreeze() {
|
|
1300
|
+
if (deepFreezeOn === void 0) {
|
|
1301
|
+
const env = typeof process === "undefined" ? void 0 : process.env.NODE_ENV;
|
|
1302
|
+
deepFreezeOn = env === "test" || env === "development";
|
|
1303
|
+
}
|
|
1304
|
+
return deepFreezeOn;
|
|
1305
|
+
}
|
|
1306
|
+
const deepFrozen = /* @__PURE__ */ new WeakSet();
|
|
1307
|
+
function deepFreeze(value) {
|
|
1308
|
+
if (value === null || typeof value !== "object" || deepFrozen.has(value)) return;
|
|
1309
|
+
deepFrozen.add(value);
|
|
1310
|
+
Object.freeze(value);
|
|
1311
|
+
for (const key of Object.keys(value)) deepFreeze(value[key]);
|
|
1312
|
+
}
|
|
1313
|
+
//#endregion
|
|
1086
1314
|
//#region src/db/meta-projection.ts
|
|
1087
1315
|
/**
|
|
1088
1316
|
* Exact-or-ancestor membership in the write whitelist: `credit.credentials.user`
|
|
@@ -1438,11 +1666,22 @@ function refTargetOf(entry) {
|
|
|
1438
1666
|
return;
|
|
1439
1667
|
}
|
|
1440
1668
|
}
|
|
1441
|
-
/**
|
|
1669
|
+
/**
|
|
1670
|
+
* Whether a no-field target type is table-served (so its read visibility must
|
|
1671
|
+
* be checked). Decided from STABLE information only, never from the runtime
|
|
1672
|
+
* `db.http.path` metadata: moost-db mirrors it after init, deletes it for an
|
|
1673
|
+
* ambiguous model and rewrites it when another app publishes, so a pure
|
|
1674
|
+
* value-help dictionary whose only `db.*` key is that mirror would flip.
|
|
1675
|
+
* Served when (1) an ARBAC controller of THIS app is registered for the type
|
|
1676
|
+
* (fail closed: guarded here => readability is checked), or (2) the schema
|
|
1677
|
+
* declares a `db.*` annotation other than `db.http.path` (`db.table`,
|
|
1678
|
+
* `db.view`, ...).
|
|
1679
|
+
*/
|
|
1442
1680
|
function isDbServed(type) {
|
|
1681
|
+
if (isArbacServedType(type)) return true;
|
|
1443
1682
|
const meta = type.metadata;
|
|
1444
1683
|
if (!meta || typeof meta.keys !== "function") return false;
|
|
1445
|
-
for (const key of meta.keys()) if (key.startsWith("db.")) return true;
|
|
1684
|
+
for (const key of meta.keys()) if (key.startsWith("db.") && key !== "db.http.path") return true;
|
|
1446
1685
|
return false;
|
|
1447
1686
|
}
|
|
1448
1687
|
/**
|
|
@@ -1630,30 +1869,45 @@ async function applyArbacMetaOverlay(meta, source, _indexes) {
|
|
|
1630
1869
|
const instance = useControllerContext().getController();
|
|
1631
1870
|
const { methods } = controllerMethodIndex(instance);
|
|
1632
1871
|
const crudKeys = Object.keys(meta.crud);
|
|
1633
|
-
const [allowedActions, crudScopes] = await Promise.all([
|
|
1872
|
+
const [allowedActions, crudScopes, arbac] = await Promise.all([
|
|
1873
|
+
grantedActions(instance, meta.actions.map((entry) => entry.name)),
|
|
1874
|
+
Promise.all(crudKeys.map((op) => evaluateHandlers(crudHandlerIds(instance, op, methods)))),
|
|
1875
|
+
currentMoostArbac()
|
|
1876
|
+
]);
|
|
1877
|
+
const memoMax = arbac.getMetaMemoMax();
|
|
1634
1878
|
const allowed = new Set(allowedActions);
|
|
1635
|
-
const
|
|
1636
|
-
const
|
|
1879
|
+
const actionBits = meta.actions.map((entry) => allowed.has(entry.name));
|
|
1880
|
+
const crudBits = crudScopes.map((scopes) => scopes !== void 0);
|
|
1637
1881
|
const readScopes = [];
|
|
1638
1882
|
const writeScopes = [];
|
|
1639
1883
|
for (let i = 0; i < crudKeys.length; i++) {
|
|
1640
1884
|
const scopes = crudScopes[i];
|
|
1641
1885
|
if (!scopes) continue;
|
|
1642
|
-
crud[crudKeys[i]] = meta.crud[crudKeys[i]];
|
|
1643
1886
|
(WRITE_CRUD_OPS.has(crudKeys[i]) ? writeScopes : readScopes).push(...scopes);
|
|
1644
1887
|
}
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1888
|
+
const overlay = () => {
|
|
1889
|
+
const crud = {};
|
|
1890
|
+
for (let i = 0; i < crudKeys.length; i++) if (crudBits[i]) crud[crudKeys[i]] = meta.crud[crudKeys[i]];
|
|
1891
|
+
return {
|
|
1892
|
+
...meta,
|
|
1893
|
+
actions: meta.actions.filter((_, i) => actionBits[i]),
|
|
1894
|
+
crud
|
|
1895
|
+
};
|
|
1649
1896
|
};
|
|
1650
|
-
if (readScopes.length === 0) return
|
|
1897
|
+
if (readScopes.length === 0) return memoMetaProjection(meta, memoMax > 0 ? metaDecisionKey({
|
|
1898
|
+
actions: actionBits,
|
|
1899
|
+
crud: crudBits,
|
|
1900
|
+
readScopes,
|
|
1901
|
+
writeScopes
|
|
1902
|
+
}, RECORD_CTX) : void 0, memoMax, overlay);
|
|
1651
1903
|
const groupBy = effectiveScope(readScopes).controls.$groupBy;
|
|
1652
1904
|
const writable = collectWritableFields(writeScopes, false);
|
|
1653
1905
|
const nestedWrites = effectiveScope(writeScopes).nestedWrites;
|
|
1654
1906
|
if (isIdentifierSet(source)) {
|
|
1655
1907
|
const vis = buildScopeVisibility(readScopes, void 0, source);
|
|
1656
|
-
|
|
1908
|
+
const visible = Object.keys(vis.allowed).length > 0 || vis.withGrants.size > 0;
|
|
1909
|
+
const overlaid = overlay();
|
|
1910
|
+
return gateGroupable(visible ? pruneMetaByVisibility(overlaid, {
|
|
1657
1911
|
...vis,
|
|
1658
1912
|
writable,
|
|
1659
1913
|
nestedWrites
|
|
@@ -1661,12 +1915,30 @@ async function applyArbacMetaOverlay(meta, source, _indexes) {
|
|
|
1661
1915
|
}
|
|
1662
1916
|
const vis = buildScopeVisibility(readScopes, source, { relations: await resolveRelationTree(readScopes, source, metaRelationNames(meta), { listSiblings: false }) });
|
|
1663
1917
|
const refTargets = await resolveRefTargets(vis, meta);
|
|
1664
|
-
return
|
|
1918
|
+
return memoMetaProjection(meta, memoMax > 0 ? metaDecisionKey({
|
|
1919
|
+
actions: actionBits,
|
|
1920
|
+
crud: crudBits,
|
|
1921
|
+
readScopes,
|
|
1922
|
+
writeScopes,
|
|
1923
|
+
projection: {
|
|
1924
|
+
vis,
|
|
1925
|
+
tree: metaRelationNames(meta),
|
|
1926
|
+
writable,
|
|
1927
|
+
nestedWrites,
|
|
1928
|
+
groupBy,
|
|
1929
|
+
refTargets
|
|
1930
|
+
}
|
|
1931
|
+
}, RECORD_CTX) : void 0, memoMax, () => gateGroupable(pruneMetaByVisibility(overlay(), {
|
|
1665
1932
|
...vis,
|
|
1666
1933
|
writable,
|
|
1667
1934
|
nestedWrites
|
|
1668
|
-
}, refTargets), groupBy);
|
|
1935
|
+
}, refTargets), groupBy));
|
|
1669
1936
|
}
|
|
1937
|
+
/** What the `/meta` memo's decision record reads from the pruning rules. */
|
|
1938
|
+
const RECORD_CTX = {
|
|
1939
|
+
isServed: isDbServed,
|
|
1940
|
+
identifiers: identifierSet
|
|
1941
|
+
};
|
|
1670
1942
|
/**
|
|
1671
1943
|
* The `/meta/form/:name` overlay: a form is a plain annotated type (no table),
|
|
1672
1944
|
* so only its cross-table pointers are projected. Every prop's `ref` and
|
|
@@ -2577,8 +2849,9 @@ function insertRowsVerdict(rows, check) {
|
|
|
2577
2849
|
if (error instanceof UnsupportedCheckError) return void 0;
|
|
2578
2850
|
throw error;
|
|
2579
2851
|
}
|
|
2852
|
+
const fieldList = [...fields];
|
|
2580
2853
|
let verdict = true;
|
|
2581
|
-
for (const row of rows) if (
|
|
2854
|
+
for (const row of rows) if (fieldList.some((f) => getPath(row, f) === void 0)) verdict = void 0;
|
|
2582
2855
|
else if (!predicate(row)) return false;
|
|
2583
2856
|
return verdict;
|
|
2584
2857
|
}
|
|
@@ -3172,11 +3445,21 @@ function requestsField(requested, field) {
|
|
|
3172
3445
|
async function applyValueHelpMetaOverlay(host, meta) {
|
|
3173
3446
|
const overlaid = await applyArbacMetaOverlay(meta, host.source);
|
|
3174
3447
|
if (!overlaid.searchable || overlaid.fields === meta.fields) return overlaid;
|
|
3175
|
-
|
|
3448
|
+
if (host.searchableFields.some((f) => f in overlaid.fields)) return overlaid;
|
|
3449
|
+
if (!Object.isFrozen(overlaid)) return {
|
|
3176
3450
|
...overlaid,
|
|
3177
3451
|
searchable: false
|
|
3178
3452
|
};
|
|
3453
|
+
let perHost = unsearchable.get(overlaid);
|
|
3454
|
+
if (!perHost) unsearchable.set(overlaid, perHost = /* @__PURE__ */ new WeakMap());
|
|
3455
|
+
let out = perHost.get(host);
|
|
3456
|
+
if (!out) perHost.set(host, out = shareMeta({
|
|
3457
|
+
...overlaid,
|
|
3458
|
+
searchable: false
|
|
3459
|
+
}));
|
|
3460
|
+
return out;
|
|
3179
3461
|
}
|
|
3462
|
+
const unsearchable = /* @__PURE__ */ new WeakMap();
|
|
3180
3463
|
let AsArbacValueHelpController = class AsArbacValueHelpController extends AsValueHelpController {
|
|
3181
3464
|
_arbacHost;
|
|
3182
3465
|
/** Makes this controller the binding / `ref` policy source for its bound type's ARBAC resource. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/arbac-moost",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.78",
|
|
4
4
|
"description": "Moost RBAC integration for aoothjs (migrated from @moostjs/arbac)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abac",
|
|
@@ -60,31 +60,31 @@
|
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@uniqu/core": "^0.1.
|
|
64
|
-
"@aooth/
|
|
65
|
-
"@aooth/
|
|
66
|
-
"@aooth/arbac-core": "0.1.
|
|
63
|
+
"@uniqu/core": "^0.1.14",
|
|
64
|
+
"@aooth/arbac": "0.1.78",
|
|
65
|
+
"@aooth/user": "0.1.78",
|
|
66
|
+
"@aooth/arbac-core": "0.1.78"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@atscript/core": "^0.1.
|
|
70
|
-
"@atscript/db": "^0.1.
|
|
71
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
72
|
-
"@atscript/db-sqlite": "^0.1.
|
|
73
|
-
"@atscript/moost-db": "^0.1.
|
|
74
|
-
"@atscript/typescript": "^0.1.
|
|
75
|
-
"@moostjs/event-http": "^0.6.
|
|
69
|
+
"@atscript/core": "^0.1.102",
|
|
70
|
+
"@atscript/db": "^0.1.152",
|
|
71
|
+
"@atscript/db-sql-tools": "^0.1.152",
|
|
72
|
+
"@atscript/db-sqlite": "^0.1.152",
|
|
73
|
+
"@atscript/moost-db": "^0.1.152",
|
|
74
|
+
"@atscript/typescript": "^0.1.102",
|
|
75
|
+
"@moostjs/event-http": "^0.6.48",
|
|
76
76
|
"@types/better-sqlite3": "^7.6.13",
|
|
77
77
|
"better-sqlite3": "^12.6.2",
|
|
78
|
-
"unplugin-atscript": "^0.1.
|
|
78
|
+
"unplugin-atscript": "^0.1.102"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
|
-
"@atscript/db": "^0.1.
|
|
82
|
-
"@atscript/moost-db": "^0.1.
|
|
83
|
-
"@atscript/typescript": "^0.1.
|
|
84
|
-
"@moostjs/event-http": "^0.6.
|
|
85
|
-
"@wooksjs/event-core": "^0.7.
|
|
86
|
-
"@wooksjs/event-http": "^0.7.
|
|
87
|
-
"moost": "^0.6.
|
|
81
|
+
"@atscript/db": "^0.1.152",
|
|
82
|
+
"@atscript/moost-db": "^0.1.152",
|
|
83
|
+
"@atscript/typescript": "^0.1.102",
|
|
84
|
+
"@moostjs/event-http": "^0.6.48",
|
|
85
|
+
"@wooksjs/event-core": "^0.7.27",
|
|
86
|
+
"@wooksjs/event-http": "^0.7.27",
|
|
87
|
+
"moost": "^0.6.48"
|
|
88
88
|
},
|
|
89
89
|
"peerDependenciesMeta": {
|
|
90
90
|
"@atscript/db": {
|