@aooth/arbac-moost 0.1.85 → 0.2.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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as AoothArbacClaims, t as ArbacUserProvider } from "../user.provider-
|
|
1
|
+
import { r as AoothArbacClaims, t as ArbacUserProvider } from "../user.provider-SisapkME.mjs";
|
|
2
2
|
import { TAtscriptAnnotatedType, TAtscriptTypeObject, TMetadataMap, TValidatorOptions, Validator } from "@atscript/typescript/utils";
|
|
3
3
|
import { AoothUserCredentials } from "@aooth/user/atscript-db/model.as";
|
|
4
4
|
//#region src/atscript/auto-provider.d.ts
|
|
@@ -196,44 +196,6 @@ declare function validateAttenuationTargets(credType: TAtscriptAnnotatedType, va
|
|
|
196
196
|
*/
|
|
197
197
|
declare function extractAttenuation(credType: TAtscriptAnnotatedType, record: object | null | undefined): AoothArbacClaims | undefined;
|
|
198
198
|
//#endregion
|
|
199
|
-
//#region src/atscript/credential-metadata-spec.d.ts
|
|
200
|
-
/**
|
|
201
|
-
* Resolved credential-METADATA column for a credential model, discovered from
|
|
202
|
-
* the `@aooth.auth.metadata` annotation (registered by `arbacPlugin`).
|
|
203
|
-
* Computed once per `TAtscriptAnnotatedType` and cached.
|
|
204
|
-
*
|
|
205
|
-
* - `metadataField` — the field annotated `@aooth.auth.metadata` **and**
|
|
206
|
-
* carrying `@db.json`. `undefined` when no field is annotated, or when the
|
|
207
|
-
* annotated field lacks `@db.json` (metadata persistence is then DISABLED —
|
|
208
|
-
* see `warnings`). Pass it straight to `CredentialStoreAtscriptDb` as its
|
|
209
|
-
* `metadataField` option — the store stays name-agnostic; this spec owns the
|
|
210
|
-
* annotation semantics (same split as `getAoothUserHandleSpec` /
|
|
211
|
-
* `UserStore.handleFields`).
|
|
212
|
-
* - `warnings` — non-fatal misconfig notes (an annotated field without
|
|
213
|
-
* `@db.json` → metadata persistence disabled, not fatal). The wiring layer
|
|
214
|
-
* should log these at boot. Ambiguity (more than one annotated field) is
|
|
215
|
-
* fatal and throws.
|
|
216
|
-
*
|
|
217
|
-
* The `@db.json` gate mirrors the handle spec's unique-index posture: metadata
|
|
218
|
-
* is a structured object, and a non-json column would either reject the write
|
|
219
|
-
* or silently stringify it engine-dependently — so a missing `@db.json`
|
|
220
|
-
* disables rather than silently accepts. A model with no annotated field
|
|
221
|
-
* resolves to `undefined` with NO warning here (graceful degradation — the
|
|
222
|
-
* consumer may simply not persist metadata); the wiring layer decides whether
|
|
223
|
-
* that deserves a boot-time note.
|
|
224
|
-
*/
|
|
225
|
-
interface AoothCredentialMetadataSpec {
|
|
226
|
-
metadataField: string | undefined;
|
|
227
|
-
warnings: string[];
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Walk a credential model's `@aooth.auth.metadata` annotation into a cached
|
|
231
|
-
* {@link AoothCredentialMetadataSpec}. Mirrors {@link getAoothUserHandleSpec}:
|
|
232
|
-
* structural, computed once, fail-loud on ambiguity, warn-and-disable on a
|
|
233
|
-
* misconfigured (non-`@db.json`) field.
|
|
234
|
-
*/
|
|
235
|
-
declare function getAoothCredentialMetadataSpec(credentialType: TAtscriptAnnotatedType): AoothCredentialMetadataSpec;
|
|
236
|
-
//#endregion
|
|
237
199
|
//#region src/atscript/handle-spec.d.ts
|
|
238
200
|
/**
|
|
239
201
|
* Resolved login/recovery HANDLE fields for a user credential model, discovered
|
|
@@ -291,4 +253,4 @@ declare class AoothArbacUserCredentials extends AoothUserCredentials {
|
|
|
291
253
|
}
|
|
292
254
|
// prettier-ignore-end
|
|
293
255
|
//#endregion
|
|
294
|
-
export { AoothArbacUserCredentials, type
|
|
256
|
+
export { AoothArbacUserCredentials, type AoothUserHandleSpec, type ArbacAttenuationSpec, ArbacUserRecordCache, type ArbacUserRecordCacheOptions, type ArbacUserTable, AtscriptArbacUserProvider, type AtscriptArbacUserProviderOptions, extractAttenuation, getAoothUserHandleSpec, getArbacAttenuationSpec, validateAttenuationTargets };
|
package/dist/atscript/index.mjs
CHANGED
|
@@ -23,7 +23,7 @@ function uniqueStrings(values) {
|
|
|
23
23
|
}
|
|
24
24
|
//#endregion
|
|
25
25
|
//#region src/atscript/auto-provider.ts
|
|
26
|
-
const specCache$
|
|
26
|
+
const specCache$2 = /* @__PURE__ */ new WeakMap();
|
|
27
27
|
/**
|
|
28
28
|
* Resolve the identifier field on an object type using the three-step chain
|
|
29
29
|
* documented on `ArbacExtractSpec.userIdField`. Used for the user type AND
|
|
@@ -62,7 +62,7 @@ function resolveIdentifierField(type) {
|
|
|
62
62
|
return metaId;
|
|
63
63
|
}
|
|
64
64
|
function getArbacExtractSpec(type) {
|
|
65
|
-
const cached = specCache$
|
|
65
|
+
const cached = specCache$2.get(type);
|
|
66
66
|
if (cached !== void 0) return cached;
|
|
67
67
|
const spec = {
|
|
68
68
|
userIdField: void 0,
|
|
@@ -71,7 +71,7 @@ function getArbacExtractSpec(type) {
|
|
|
71
71
|
};
|
|
72
72
|
const def = type.type;
|
|
73
73
|
if (def.kind !== "object") {
|
|
74
|
-
specCache$
|
|
74
|
+
specCache$2.set(type, spec);
|
|
75
75
|
return spec;
|
|
76
76
|
}
|
|
77
77
|
const roleCandidates = [];
|
|
@@ -102,7 +102,7 @@ function getArbacExtractSpec(type) {
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
spec.userIdField = resolveIdentifierField(type);
|
|
105
|
-
specCache$
|
|
105
|
+
specCache$2.set(type, spec);
|
|
106
106
|
return spec;
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
@@ -310,14 +310,14 @@ AtscriptArbacUserProvider = __decorate([Injectable(), __decorateMetadata("design
|
|
|
310
310
|
])], AtscriptArbacUserProvider);
|
|
311
311
|
//#endregion
|
|
312
312
|
//#region src/atscript/attenuation-extract.ts
|
|
313
|
-
const specCache$
|
|
313
|
+
const specCache$1 = /* @__PURE__ */ new WeakMap();
|
|
314
314
|
/**
|
|
315
315
|
* Walk a credential model's `@arbac.attenuate.*` annotations into a cached
|
|
316
316
|
* {@link ArbacAttenuationSpec}. Mirrors the user-side `getArbacExtractSpec`:
|
|
317
317
|
* structural, computed once, fail-loud on an ambiguous (multiple) role field.
|
|
318
318
|
*/
|
|
319
319
|
function getArbacAttenuationSpec(credType) {
|
|
320
|
-
const cached = specCache$
|
|
320
|
+
const cached = specCache$1.get(credType);
|
|
321
321
|
if (cached !== void 0) return cached;
|
|
322
322
|
const spec = {
|
|
323
323
|
roleField: void 0,
|
|
@@ -325,7 +325,7 @@ function getArbacAttenuationSpec(credType) {
|
|
|
325
325
|
};
|
|
326
326
|
const def = credType.type;
|
|
327
327
|
if (def.kind !== "object") {
|
|
328
|
-
specCache$
|
|
328
|
+
specCache$1.set(credType, spec);
|
|
329
329
|
return spec;
|
|
330
330
|
}
|
|
331
331
|
const roleCandidates = [];
|
|
@@ -340,7 +340,7 @@ function getArbacAttenuationSpec(credType) {
|
|
|
340
340
|
}
|
|
341
341
|
if (roleCandidates.length > 1) throw new Error(`getArbacAttenuationSpec: multiple @arbac.attenuate.role fields declared (${roleCandidates.join(", ")}). Exactly one assumed-role source is supported — drop @arbac.attenuate.role from all but the canonical field.`);
|
|
342
342
|
if (roleCandidates.length === 1) spec.roleField = roleCandidates[0];
|
|
343
|
-
specCache$
|
|
343
|
+
specCache$1.set(credType, spec);
|
|
344
344
|
return spec;
|
|
345
345
|
}
|
|
346
346
|
/**
|
|
@@ -404,44 +404,6 @@ function extractAttenuation(credType, record) {
|
|
|
404
404
|
return out;
|
|
405
405
|
}
|
|
406
406
|
//#endregion
|
|
407
|
-
//#region src/atscript/credential-metadata-spec.ts
|
|
408
|
-
const specCache$1 = /* @__PURE__ */ new WeakMap();
|
|
409
|
-
/**
|
|
410
|
-
* Walk a credential model's `@aooth.auth.metadata` annotation into a cached
|
|
411
|
-
* {@link AoothCredentialMetadataSpec}. Mirrors {@link getAoothUserHandleSpec}:
|
|
412
|
-
* structural, computed once, fail-loud on ambiguity, warn-and-disable on a
|
|
413
|
-
* misconfigured (non-`@db.json`) field.
|
|
414
|
-
*/
|
|
415
|
-
function getAoothCredentialMetadataSpec(credentialType) {
|
|
416
|
-
const cached = specCache$1.get(credentialType);
|
|
417
|
-
if (cached !== void 0) return cached;
|
|
418
|
-
const spec = {
|
|
419
|
-
metadataField: void 0,
|
|
420
|
-
warnings: []
|
|
421
|
-
};
|
|
422
|
-
const def = credentialType.type;
|
|
423
|
-
if (def.kind !== "object") {
|
|
424
|
-
specCache$1.set(credentialType, spec);
|
|
425
|
-
return spec;
|
|
426
|
-
}
|
|
427
|
-
const candidates = [];
|
|
428
|
-
for (const [fieldName, fieldType] of def.props) {
|
|
429
|
-
const md = fieldType.metadata;
|
|
430
|
-
if (md.get("aooth.auth.metadata")) candidates.push({
|
|
431
|
-
field: fieldName,
|
|
432
|
-
md
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
if (candidates.length > 1) throw new Error(`getAoothCredentialMetadataSpec: multiple @aooth.auth.metadata fields declared (${candidates.map((c) => c.field).join(", ")}). Exactly one credential-metadata column is supported — drop @aooth.auth.metadata from all but the canonical field.`);
|
|
436
|
-
if (candidates.length === 1) {
|
|
437
|
-
const { field, md } = candidates[0];
|
|
438
|
-
if (md.get("db.json")) spec.metadataField = field;
|
|
439
|
-
else spec.warnings.push(`@aooth.auth.metadata on field "${field}" has no @db.json — a structured metadata object needs a json column, so metadata persistence is DISABLED (the atscript-db credential store will not persist/read metadata). Add @db.json to the field to enable it.`);
|
|
440
|
-
}
|
|
441
|
-
specCache$1.set(credentialType, spec);
|
|
442
|
-
return spec;
|
|
443
|
-
}
|
|
444
|
-
//#endregion
|
|
445
407
|
//#region src/atscript/handle-spec.ts
|
|
446
408
|
const specCache = /* @__PURE__ */ new WeakMap();
|
|
447
409
|
/** A `@db.index.unique` annotation is a non-empty `(string | true)[]`. */
|
|
@@ -515,6 +477,6 @@ var AoothArbacUserCredentials = class {
|
|
|
515
477
|
throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
|
|
516
478
|
}
|
|
517
479
|
};
|
|
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);
|
|
480
|
+
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("invitedBy", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("inviteNonce", defineAnnotatedType().designType("string").tags("string").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);
|
|
519
481
|
//#endregion
|
|
520
|
-
export { AoothArbacUserCredentials, ArbacUserRecordCache, AtscriptArbacUserProvider, extractAttenuation,
|
|
482
|
+
export { AoothArbacUserCredentials, ArbacUserRecordCache, AtscriptArbacUserProvider, extractAttenuation, getAoothUserHandleSpec, getArbacAttenuationSpec, validateAttenuationTargets };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as ProjectionOf, a as conjoinArbacDbScopes, b as TArbacMeta, c as ArbacDbScope, d as extractUsedControlValues, f as applyAllowedFieldsAndSet, g as OwnFieldKey, h as NavTarget, i as ConjoinArbacDbScopesOptions, l as AsArbacDbController, m as NavRelationKey, n as ArbacUserProviderToken, o as RefForeignKey, p as ControlsOf, r as AoothArbacClaims, s as RefTableSource, t as ArbacUserProvider, u as enforceControlsPolicy, v as ARBAC_DELEGATED_AUTH, x as getArbacMate, y as ArbacDelegatedAuth } from "./user.provider-
|
|
1
|
+
import { _ as ProjectionOf, a as conjoinArbacDbScopes, b as TArbacMeta, c as ArbacDbScope, d as extractUsedControlValues, f as applyAllowedFieldsAndSet, g as OwnFieldKey, h as NavTarget, i as ConjoinArbacDbScopesOptions, l as AsArbacDbController, m as NavRelationKey, n as ArbacUserProviderToken, o as RefForeignKey, p as ControlsOf, r as AoothArbacClaims, s as RefTableSource, t as ArbacUserProvider, u as enforceControlsPolicy, v as ARBAC_DELEGATED_AUTH, x as getArbacMate, y as ArbacDelegatedAuth } from "./user.provider-SisapkME.mjs";
|
|
2
2
|
import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacEvalResult as TArbacEvalResult$1, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
|
|
3
3
|
import { ControlGate, ControlGate as ControlGate$1, TProjection, TScopeFieldRules, TScopeFilter, conjoinScopeFilters } from "@aooth/arbac";
|
|
4
4
|
import { TAuthGuardDef } from "@moostjs/event-http";
|
|
@@ -112,6 +112,13 @@ interface MetaVisibility {
|
|
|
112
112
|
* behind a read projection). Absent → nothing extra survives.
|
|
113
113
|
*/
|
|
114
114
|
writable?: ReadonlySet<string> | "all";
|
|
115
|
+
/**
|
|
116
|
+
* Fields the WRITE scopes pin via `set` (keys of the effective `set`
|
|
117
|
+
* overlay). The server overwrites whatever a client sends there, so a forced
|
|
118
|
+
* field is never a write affordance: hidden → pruned from `/meta`; visible →
|
|
119
|
+
* kept and stamped `meta.readonly`. A path under a forced key is forced too.
|
|
120
|
+
*/
|
|
121
|
+
forced?: ReadonlySet<string>;
|
|
115
122
|
/**
|
|
116
123
|
* Nav relations the WRITE scopes opt into nested writes (union of
|
|
117
124
|
* `nestedWrites`). A relation path is writable only when listed here (and
|
package/dist/index.mjs
CHANGED
|
@@ -696,8 +696,11 @@ function prefetchRefTargets(scopes, table) {
|
|
|
696
696
|
* its `fields` rule; without one this THROWS — never dropped (fail closed).
|
|
697
697
|
*
|
|
698
698
|
* The third argument is the options object, or (legacy form) `childrenOf`.
|
|
699
|
+
* Either list empty THROWS (since 0.2): an empty list means that pass denied,
|
|
700
|
+
* and must not conjoin as unrestricted.
|
|
699
701
|
*/
|
|
700
702
|
function conjoinArbacDbScopes(userScopes, credScopes, opts) {
|
|
703
|
+
if (userScopes.length === 0 || credScopes.length === 0) throw new Error("conjoinArbacDbScopes: an empty scope list is a denied evaluation");
|
|
701
704
|
const { childrenOf, refTable, fields } = typeof opts === "function" ? { childrenOf: opts } : opts ?? {};
|
|
702
705
|
return [conjoinScopes(userScopes, credScopes, {
|
|
703
706
|
childrenOf,
|
|
@@ -1104,7 +1107,7 @@ const arbacAuthorizeInterceptor = Object.assign(defineBeforeInterceptor(async ()
|
|
|
1104
1107
|
} catch (error) {
|
|
1105
1108
|
if (error instanceof HttpError) throw error;
|
|
1106
1109
|
logger.warn(String(error));
|
|
1107
|
-
throw new HttpError(401,
|
|
1110
|
+
throw new HttpError(401, "Not authorized");
|
|
1108
1111
|
}
|
|
1109
1112
|
}, TInterceptorPriority.GUARD), { __authTransports: {} });
|
|
1110
1113
|
function delegatesToArbacPrepareRequest(ctx) {
|
|
@@ -1241,7 +1244,7 @@ function metaDecisionKey(input, ctx) {
|
|
|
1241
1244
|
const vis = p.refTargets.get(type);
|
|
1242
1245
|
refTargets.push(idOf(type), ctx.isServed(type), vis ? encodeLevel(vis, NO_TREE, ctx, seen, 1) : 0);
|
|
1243
1246
|
}
|
|
1244
|
-
const { writable, nestedWrites } = p;
|
|
1247
|
+
const { writable, nestedWrites, forced } = p;
|
|
1245
1248
|
const controls = [];
|
|
1246
1249
|
for (const op of Object.keys(p.controls).toSorted()) {
|
|
1247
1250
|
const policy = p.controls[op] ?? {};
|
|
@@ -1259,6 +1262,7 @@ function metaDecisionKey(input, ctx) {
|
|
|
1259
1262
|
encodeLevel(p.vis, p.tree, ctx, seen, 0),
|
|
1260
1263
|
writable === void 0 ? 0 : writable === "all" ? 1 : sortedNames(writable),
|
|
1261
1264
|
nestedWrites ? sortedNames(nestedWrites) : 0,
|
|
1265
|
+
forced ? sortedNames(forced) : 0,
|
|
1262
1266
|
controls,
|
|
1263
1267
|
refTargets,
|
|
1264
1268
|
p.vis.table?.type ? ctx.isServed(p.vis.table.type) : 0
|
|
@@ -1315,11 +1319,16 @@ function shareMeta(obj) {
|
|
|
1315
1319
|
function isPathWritable(path, vis) {
|
|
1316
1320
|
const writable = vis.writable;
|
|
1317
1321
|
if (!writable || tablePolicy(vis.table).derived.has(path)) return false;
|
|
1322
|
+
if (isForced(path, vis)) return false;
|
|
1318
1323
|
const dot = path.indexOf(".");
|
|
1319
1324
|
const head = dot === -1 ? path : path.slice(0, dot);
|
|
1320
1325
|
if ((vis.relationNames?.has(head) || vis.table?.navFields?.has(head)) && !vis.nestedWrites?.has(head)) return false;
|
|
1321
1326
|
return writable === "all" || hasSelfOrAncestor(writable, path);
|
|
1322
1327
|
}
|
|
1328
|
+
/** `path` (or an ancestor) is pinned by the write scopes' `set` — see {@link MetaVisibility.forced}. */
|
|
1329
|
+
function isForced(path, vis) {
|
|
1330
|
+
return !!vis.forced?.size && hasSelfOrAncestor(vis.forced, path);
|
|
1331
|
+
}
|
|
1323
1332
|
/**
|
|
1324
1333
|
* Prune a `/meta` envelope down to the fields the principal's read scopes can
|
|
1325
1334
|
* ever surface, so a scoped UI cannot even OFFER an out-of-scope column (the
|
|
@@ -1541,7 +1550,8 @@ function pruneSerializedType(node, level) {
|
|
|
1541
1550
|
else if (hasWritableDescendant(path, vis.writable)) props[name] = withVisibleTargets(stampWriteOnly(pruneSerializedType(prop, child)), path, vis, refTargets, level.inBranch);
|
|
1542
1551
|
continue;
|
|
1543
1552
|
}
|
|
1544
|
-
|
|
1553
|
+
const pruned = pruneSerializedType(prop, child);
|
|
1554
|
+
props[name] = withVisibleTargets(isForced(path, vis) ? stampReadOnly(pruned) : pruned, path, vis, refTargets, level.inBranch);
|
|
1545
1555
|
}
|
|
1546
1556
|
const pruned = {
|
|
1547
1557
|
...node,
|
|
@@ -1581,6 +1591,15 @@ function stampWriteOnly(node) {
|
|
|
1581
1591
|
}
|
|
1582
1592
|
};
|
|
1583
1593
|
}
|
|
1594
|
+
function stampReadOnly(node) {
|
|
1595
|
+
return {
|
|
1596
|
+
...node,
|
|
1597
|
+
metadata: {
|
|
1598
|
+
...node.metadata,
|
|
1599
|
+
"meta.readonly": true
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1584
1603
|
/** A strict descendant of `path` is in the write whitelist. */
|
|
1585
1604
|
function hasWritableDescendant(path, writable) {
|
|
1586
1605
|
if (!writable || writable === "all") return false;
|
|
@@ -1904,7 +1923,9 @@ async function applyArbacMetaOverlay(meta, source, _indexes) {
|
|
|
1904
1923
|
writeScopes
|
|
1905
1924
|
}, RECORD_CTX) : void 0, memoMax, overlay);
|
|
1906
1925
|
const writable = collectWritableFields(writeScopes, false);
|
|
1907
|
-
const
|
|
1926
|
+
const writeEffective = effectiveScope(writeScopes);
|
|
1927
|
+
const nestedWrites = writeEffective.nestedWrites;
|
|
1928
|
+
const forced = writeEffective.set ? new Set(Object.keys(writeEffective.set)) : void 0;
|
|
1908
1929
|
if (isIdentifierSet(source)) {
|
|
1909
1930
|
const vis = buildScopeVisibility(readScopes, void 0, source);
|
|
1910
1931
|
const visible = Object.keys(vis.allowed).length > 0 || vis.withGrants.size > 0;
|
|
@@ -1912,7 +1933,8 @@ async function applyArbacMetaOverlay(meta, source, _indexes) {
|
|
|
1912
1933
|
return gateMetaControls(visible ? pruneMetaByVisibility(overlaid, {
|
|
1913
1934
|
...vis,
|
|
1914
1935
|
writable,
|
|
1915
|
-
nestedWrites
|
|
1936
|
+
nestedWrites,
|
|
1937
|
+
forced
|
|
1916
1938
|
}) : overlaid, controls);
|
|
1917
1939
|
}
|
|
1918
1940
|
const vis = buildScopeVisibility(readScopes, source, { relations: await resolveRelationTree(readScopes, source, metaRelationNames(meta), { listSiblings: false }) });
|
|
@@ -1927,13 +1949,15 @@ async function applyArbacMetaOverlay(meta, source, _indexes) {
|
|
|
1927
1949
|
tree: metaRelationNames(meta),
|
|
1928
1950
|
writable,
|
|
1929
1951
|
nestedWrites,
|
|
1952
|
+
forced,
|
|
1930
1953
|
controls,
|
|
1931
1954
|
refTargets
|
|
1932
1955
|
}
|
|
1933
1956
|
}, RECORD_CTX) : void 0, memoMax, () => gateMetaControls(pruneMetaByVisibility(overlay(), {
|
|
1934
1957
|
...vis,
|
|
1935
1958
|
writable,
|
|
1936
|
-
nestedWrites
|
|
1959
|
+
nestedWrites,
|
|
1960
|
+
forced
|
|
1937
1961
|
}, refTargets), controls));
|
|
1938
1962
|
}
|
|
1939
1963
|
/** What the `/meta` memo's decision record reads from the pruning rules. */
|
|
@@ -3410,8 +3434,10 @@ function enforceControlsPolicy(policy, controls) {
|
|
|
3410
3434
|
*
|
|
3411
3435
|
* Currently supported (matches `WHITELISTABLE_CONTROLS` in `unionControlsPolicy`):
|
|
3412
3436
|
* - `$with` — array of `{ name, … }` objects (per `TypedWithRelation`,
|
|
3413
|
-
* see `@uniqu/core` parser at `parseWithSegment`); we extract `name
|
|
3414
|
-
*
|
|
3437
|
+
* see `@uniqu/core` parser at `parseWithSegment`); we extract `name`,
|
|
3438
|
+
* and walk nested `controls.$with` emitting dotted paths
|
|
3439
|
+
* (`author($with=org)` → `author`, `author.org`) so a whitelist can name
|
|
3440
|
+
* a nested relation. Bare strings are tolerated for forward compatibility.
|
|
3415
3441
|
* - `$groupBy` — array of column names (strings). With `controls` given,
|
|
3416
3442
|
* a calendar-bucket alias (`$select: [{ $bucket, $field, $as }]`) is
|
|
3417
3443
|
* mapped to its source `$field` (`groupByFields` from `@uniqu/core`),
|
|
@@ -3423,18 +3449,27 @@ function enforceControlsPolicy(policy, controls) {
|
|
|
3423
3449
|
*/
|
|
3424
3450
|
function extractUsedControlValues(key, value, controls) {
|
|
3425
3451
|
if (!Array.isArray(value)) return [];
|
|
3426
|
-
if (key === "$with")
|
|
3427
|
-
const out = [];
|
|
3428
|
-
for (const entry of value) if (typeof entry === "string") out.push(entry);
|
|
3429
|
-
else if (typeof entry?.name === "string") out.push(entry.name);
|
|
3430
|
-
return out;
|
|
3431
|
-
}
|
|
3452
|
+
if (key === "$with") return withPaths(value, "");
|
|
3432
3453
|
if (key === "$groupBy") return controls ? groupByFields({
|
|
3433
3454
|
$select: controls.$select,
|
|
3434
3455
|
$groupBy: value
|
|
3435
3456
|
}) : value.filter((x) => typeof x === "string");
|
|
3436
3457
|
return [];
|
|
3437
3458
|
}
|
|
3459
|
+
/** Relation paths of a `$with` list, nested `controls.$with` as dotted paths. */
|
|
3460
|
+
function withPaths(list, prefix) {
|
|
3461
|
+
const out = [];
|
|
3462
|
+
for (const entry of list) {
|
|
3463
|
+
const rel = entry;
|
|
3464
|
+
const name = typeof entry === "string" ? entry : rel?.name;
|
|
3465
|
+
if (typeof name !== "string") continue;
|
|
3466
|
+
const path = prefix + name;
|
|
3467
|
+
out.push(path);
|
|
3468
|
+
const nested = rel?.controls?.$with;
|
|
3469
|
+
if (Array.isArray(nested)) out.push(...withPaths(nested, `${path}.`));
|
|
3470
|
+
}
|
|
3471
|
+
return out;
|
|
3472
|
+
}
|
|
3438
3473
|
/**
|
|
3439
3474
|
* Translate a `@uniqu/url` parser `SyntaxError` into `HttpError(400)`. Any
|
|
3440
3475
|
* other error (including a pre-existing `HttpError`) is returned as-is so the
|
package/dist/plugin.mjs
CHANGED
|
@@ -105,7 +105,7 @@ function arbacPlugin() {
|
|
|
105
105
|
})
|
|
106
106
|
},
|
|
107
107
|
auth: { metadata: new AnnotationSpec({
|
|
108
|
-
description: "Marks the consumer's fully-typed credential-metadata column (@db.json) on their `extends AoothAuthCredential` model — the runtime/validation twin of the `CredentialMetadata` declaration merge.
|
|
108
|
+
description: "Marks the consumer's fully-typed credential-metadata column (@db.json) on their `extends AoothAuthCredential` model — the runtime/validation twin of the `CredentialMetadata` declaration merge. `CredentialStoreAtscriptDb` finds it on `table.type` automatically (`getAoothCredentialMetadataSpec` from `@aooth/auth/atscript-db`). The field MUST carry @db.json (a structured metadata object needs a json column — without it, metadata persistence is DISABLED with a warning). At most one @aooth.auth.metadata field per type — multiple declarations throw at boot. Absent → the store refuses credentials whose metadata carries per-token restrictions (refresh rotation, access TTL, credential kind, OAuth client binding) and drops the rest with a warning.",
|
|
109
109
|
nodeType: ["prop"],
|
|
110
110
|
passedWhenReferred: false,
|
|
111
111
|
multiple: false
|
|
@@ -428,8 +428,10 @@ declare function enforceControlsPolicy(policy: Record<string, ControlGate>, cont
|
|
|
428
428
|
*
|
|
429
429
|
* Currently supported (matches `WHITELISTABLE_CONTROLS` in `unionControlsPolicy`):
|
|
430
430
|
* - `$with` — array of `{ name, … }` objects (per `TypedWithRelation`,
|
|
431
|
-
* see `@uniqu/core` parser at `parseWithSegment`); we extract `name
|
|
432
|
-
*
|
|
431
|
+
* see `@uniqu/core` parser at `parseWithSegment`); we extract `name`,
|
|
432
|
+
* and walk nested `controls.$with` emitting dotted paths
|
|
433
|
+
* (`author($with=org)` → `author`, `author.org`) so a whitelist can name
|
|
434
|
+
* a nested relation. Bare strings are tolerated for forward compatibility.
|
|
433
435
|
* - `$groupBy` — array of column names (strings). With `controls` given,
|
|
434
436
|
* a calendar-bucket alias (`$select: [{ $bucket, $field, $as }]`) is
|
|
435
437
|
* mapped to its source `$field` (`groupByFields` from `@uniqu/core`),
|
|
@@ -517,6 +519,8 @@ interface AoothArbacClaims {
|
|
|
517
519
|
* its `fields` rule; without one this THROWS — never dropped (fail closed).
|
|
518
520
|
*
|
|
519
521
|
* The third argument is the options object, or (legacy form) `childrenOf`.
|
|
522
|
+
* Either list empty THROWS (since 0.2): an empty list means that pass denied,
|
|
523
|
+
* and must not conjoin as unrestricted.
|
|
520
524
|
*/
|
|
521
525
|
declare function conjoinArbacDbScopes(userScopes: ArbacDbScope[], credScopes: ArbacDbScope[], opts?: TProjectionChildren | ConjoinArbacDbScopesOptions): ArbacDbScope[];
|
|
522
526
|
/** Options of {@link conjoinArbacDbScopes}. @since 0.1.72 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/arbac-moost",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Moost RBAC integration for aoothjs (migrated from @moostjs/arbac)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abac",
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@uniqu/core": "^0.1.15",
|
|
64
|
-
"@aooth/arbac": "0.
|
|
65
|
-
"@aooth/
|
|
66
|
-
"@aooth/
|
|
64
|
+
"@aooth/arbac": "0.2.0",
|
|
65
|
+
"@aooth/arbac-core": "0.2.0",
|
|
66
|
+
"@aooth/user": "0.2.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@atscript/core": "^0.1.109",
|