@aooth/arbac-moost 0.1.84 → 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-BZkeBV5s.mjs";
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 AoothCredentialMetadataSpec, type AoothUserHandleSpec, type ArbacAttenuationSpec, ArbacUserRecordCache, type ArbacUserRecordCacheOptions, type ArbacUserTable, AtscriptArbacUserProvider, type AtscriptArbacUserProviderOptions, extractAttenuation, getAoothCredentialMetadataSpec, getAoothUserHandleSpec, getArbacAttenuationSpec, validateAttenuationTargets };
256
+ export { AoothArbacUserCredentials, type AoothUserHandleSpec, type ArbacAttenuationSpec, ArbacUserRecordCache, type ArbacUserRecordCacheOptions, type ArbacUserTable, AtscriptArbacUserProvider, type AtscriptArbacUserProviderOptions, extractAttenuation, getAoothUserHandleSpec, getArbacAttenuationSpec, validateAttenuationTargets };
@@ -23,7 +23,7 @@ function uniqueStrings(values) {
23
23
  }
24
24
  //#endregion
25
25
  //#region src/atscript/auto-provider.ts
26
- const specCache$3 = /* @__PURE__ */ new WeakMap();
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$3.get(type);
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$3.set(type, spec);
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$3.set(type, spec);
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$2 = /* @__PURE__ */ new WeakMap();
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$2.get(credType);
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$2.set(credType, spec);
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$2.set(credType, spec);
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, getAoothCredentialMetadataSpec, getAoothUserHandleSpec, getArbacAttenuationSpec, validateAttenuationTargets };
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-BZkeBV5s.mjs";
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, error instanceof Error ? error.message : String(error));
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
- props[name] = withVisibleTargets(pruneSerializedType(prop, child), path, vis, refTargets, level.inBranch);
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 nestedWrites = effectiveScope(writeScopes).nestedWrites;
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
- * Bare strings are tolerated for forward compatibility.
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. Resolved by `getAoothCredentialMetadataSpec` and threaded to `CredentialStoreAtscriptDb` as `metadataField`. 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 atscript-db credential store persists no metadata (non-fatal, warned at boot by the wiring).",
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
- * Bare strings are tolerated for forward compatibility.
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.1.84",
3
+ "version": "0.2.0",
4
4
  "description": "Moost RBAC integration for aoothjs (migrated from @moostjs/arbac)",
5
5
  "keywords": [
6
6
  "abac",
@@ -61,16 +61,16 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@uniqu/core": "^0.1.15",
64
- "@aooth/arbac-core": "0.1.84",
65
- "@aooth/arbac": "0.1.84",
66
- "@aooth/user": "0.1.84"
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",
70
- "@atscript/db": "^0.1.163",
71
- "@atscript/db-sql-tools": "^0.1.163",
72
- "@atscript/db-sqlite": "^0.1.163",
73
- "@atscript/moost-db": "^0.1.163",
70
+ "@atscript/db": "^0.1.164",
71
+ "@atscript/db-sql-tools": "^0.1.164",
72
+ "@atscript/db-sqlite": "^0.1.164",
73
+ "@atscript/moost-db": "^0.1.164",
74
74
  "@atscript/typescript": "^0.1.109",
75
75
  "@moostjs/event-http": "^0.6.53",
76
76
  "@types/better-sqlite3": "^7.6.13",
@@ -78,8 +78,8 @@
78
78
  "unplugin-atscript": "^0.1.109"
79
79
  },
80
80
  "peerDependencies": {
81
- "@atscript/db": "^0.1.163",
82
- "@atscript/moost-db": "^0.1.163",
81
+ "@atscript/db": "^0.1.164",
82
+ "@atscript/moost-db": "^0.1.164",
83
83
  "@atscript/typescript": "^0.1.109",
84
84
  "@moostjs/event-http": "^0.6.53",
85
85
  "@wooksjs/event-core": "^0.7.31",