@aooth/arbac-moost 0.1.7 → 0.1.9
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 +95 -3
- package/dist/atscript/index.mjs +191 -24
- package/dist/index.d.mts +5 -217
- package/dist/index.mjs +116 -11
- package/dist/plugin.d.mts +23 -0
- package/dist/plugin.mjs +71 -17
- package/dist/user-Dzu_08g0.mjs +15 -0
- package/dist/{user.provider-CGWueNjx.mjs → user.provider-BiM5P8fl.mjs} +1 -1
- package/dist/user.provider-CKPMp3G8.d.mts +314 -0
- package/package.json +27 -21
- package/dist/user-ChbLPYhG.mjs +0 -15
- package/dist/user.provider-JpI5dD6Z.d.mts +0 -42
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as AoothArbacUserCredentials } from "../user-
|
|
2
|
-
import { t as ArbacUserProvider } from "../user.provider-
|
|
1
|
+
import { t as AoothArbacUserCredentials } from "../user-Dzu_08g0.mjs";
|
|
2
|
+
import { r as AoothArbacClaims, t as ArbacUserProvider } from "../user.provider-CKPMp3G8.mjs";
|
|
3
3
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
4
4
|
|
|
5
5
|
//#region src/atscript/auto-provider.d.ts
|
|
@@ -88,4 +88,96 @@ declare abstract class AtscriptArbacUserProvider<T extends object = object> exte
|
|
|
88
88
|
private fetchRecord;
|
|
89
89
|
}
|
|
90
90
|
//#endregion
|
|
91
|
-
|
|
91
|
+
//#region src/atscript/attenuation-extract.d.ts
|
|
92
|
+
/**
|
|
93
|
+
* Spec computed once per credential `TAtscriptAnnotatedType` — which typed root
|
|
94
|
+
* fields carry the restrict-only ARBAC attenuation:
|
|
95
|
+
*
|
|
96
|
+
* - `roleField` — the single `@arbac.attenuate.role`-annotated prop (holds the
|
|
97
|
+
* assumed-role SUBSET, `string[]`). More than one throws at spec time.
|
|
98
|
+
* - `attrFields` — every `@arbac.attenuate.attr "userAttr"` prop, paired with
|
|
99
|
+
* the target user-attribute key it narrows (the annotation argument).
|
|
100
|
+
*/
|
|
101
|
+
interface ArbacAttenuationSpec {
|
|
102
|
+
roleField: string | undefined;
|
|
103
|
+
attrFields: Array<{
|
|
104
|
+
field: string;
|
|
105
|
+
userAttr: string;
|
|
106
|
+
}>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Walk a credential model's `@arbac.attenuate.*` annotations into a cached
|
|
110
|
+
* {@link ArbacAttenuationSpec}. Mirrors the user-side `getArbacExtractSpec`:
|
|
111
|
+
* structural, computed once, fail-loud on an ambiguous (multiple) role field.
|
|
112
|
+
*/
|
|
113
|
+
declare function getArbacAttenuationSpec(credType: TAtscriptAnnotatedType): ArbacAttenuationSpec;
|
|
114
|
+
/**
|
|
115
|
+
* Boot-time cross-model check: every `@arbac.attenuate.attr "userAttr"` target
|
|
116
|
+
* must name a real key in the user model's `@arbac.attribute` keyspace
|
|
117
|
+
* (`validUserAttrs`), else a typo silently no-ops the narrowing. Throws on the
|
|
118
|
+
* first miss. Call once at provider construction; safe and cheap (the spec is
|
|
119
|
+
* cached). Soundness does NOT depend on this — the scope conjunction clips
|
|
120
|
+
* regardless — but it turns a silent misconfig into a loud boot failure.
|
|
121
|
+
*/
|
|
122
|
+
declare function validateAttenuationTargets(credType: TAtscriptAnnotatedType, validUserAttrs: Iterable<string>): void;
|
|
123
|
+
/**
|
|
124
|
+
* Build the restrict-only {@link AoothArbacClaims} for a credential `record`
|
|
125
|
+
* (its typed root fields — e.g. the auth context, which carries the credential
|
|
126
|
+
* payload flat) by reading the fields the credential model annotates. Designed
|
|
127
|
+
* to back a consumer's `ArbacUserProvider.getAttenuation()` override while
|
|
128
|
+
* arbac-moost stays auth-agnostic (the consumer supplies the record).
|
|
129
|
+
*
|
|
130
|
+
* Returns:
|
|
131
|
+
* - `undefined` when the model declares NO attenuation fields, OR a normal
|
|
132
|
+
* (non-attenuated) token carries none of them set → full user authority.
|
|
133
|
+
* - `{ roles, attrs }` otherwise. The narrowing is then clipped by the engine's
|
|
134
|
+
* scope conjunction, so it can never widen beyond the user.
|
|
135
|
+
*
|
|
136
|
+
* Fail-closed: when the assumed-role field IS set but holds a malformed value
|
|
137
|
+
* (no usable role strings), {@link parseRoles} yields `[]` — deny-all — never
|
|
138
|
+
* full authority. Absent (`undefined`/`null`) = the field was not set, so roles
|
|
139
|
+
* are omitted (attrs-only narrowing), which is the distinct "no role narrowing"
|
|
140
|
+
* case, not a malformed one.
|
|
141
|
+
*/
|
|
142
|
+
declare function extractAttenuation(credType: TAtscriptAnnotatedType, record: object | null | undefined): AoothArbacClaims | undefined;
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/atscript/handle-spec.d.ts
|
|
145
|
+
/**
|
|
146
|
+
* Resolved login/recovery HANDLE fields for a user credential model, discovered
|
|
147
|
+
* from the `@aooth.user.*` annotations (registered by `arbacPlugin`). Computed
|
|
148
|
+
* once per `TAtscriptAnnotatedType` and cached.
|
|
149
|
+
*
|
|
150
|
+
* - `emailField` — the field annotated `@aooth.user.email` **and** carrying a
|
|
151
|
+
* `@db.index.unique` index. `undefined` when no field is annotated, or when
|
|
152
|
+
* the annotated field lacks a unique index (the handle is then DISABLED — see
|
|
153
|
+
* `warnings`).
|
|
154
|
+
* - `phoneField` — same, for `@aooth.user.phone`.
|
|
155
|
+
* - `handleFields` — the enabled handle field names in resolution order (email,
|
|
156
|
+
* then phone; disabled handles omitted). Pass this straight to a `UserStore`
|
|
157
|
+
* (`UsersStoreAtscriptDb` / `UserStoreMemory`) as its `handleFields` — the
|
|
158
|
+
* store stays name-agnostic; this spec owns the email/phone semantics.
|
|
159
|
+
* - `warnings` — non-fatal misconfig notes (an annotated field with no unique
|
|
160
|
+
* index → that handle is disabled, not fatal). The wiring layer should log
|
|
161
|
+
* these at boot. Ambiguity (more than one field per role) is fatal and throws.
|
|
162
|
+
*
|
|
163
|
+
* A handle MUST be unique-when-present because `findByHandle` resolves a handle
|
|
164
|
+
* to AT MOST one row; a non-unique handle would resolve to an arbitrary one of
|
|
165
|
+
* several rows (an account-takeover footgun). That is why the unique index is
|
|
166
|
+
* the gate, and why a missing index disables rather than silently accepts.
|
|
167
|
+
*/
|
|
168
|
+
interface AoothUserHandleSpec {
|
|
169
|
+
emailField: string | undefined;
|
|
170
|
+
phoneField: string | undefined;
|
|
171
|
+
handleFields: string[];
|
|
172
|
+
warnings: string[];
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Walk a user model's `@aooth.user.*` annotations into a cached
|
|
176
|
+
* {@link AoothUserHandleSpec}. Mirrors {@link getArbacAttenuationSpec}:
|
|
177
|
+
* structural, computed once, fail-loud on ambiguity. A model with no
|
|
178
|
+
* `@aooth.user.*` field resolves to all-`undefined` handles (email/phone login
|
|
179
|
+
* is simply unavailable — graceful degradation, not an error).
|
|
180
|
+
*/
|
|
181
|
+
declare function getAoothUserHandleSpec(userType: TAtscriptAnnotatedType): AoothUserHandleSpec;
|
|
182
|
+
//#endregion
|
|
183
|
+
export { AoothArbacUserCredentials, type AoothUserHandleSpec, type ArbacAttenuationSpec, type ArbacUserTable, AtscriptArbacUserProvider, extractAttenuation, getAoothUserHandleSpec, getArbacAttenuationSpec, validateAttenuationTargets };
|
package/dist/atscript/index.mjs
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
|
-
import { r as __decorate, t as ArbacUserProvider } from "../user.provider-
|
|
2
|
-
import { t as AoothArbacUserCredentials } from "../user-
|
|
1
|
+
import { r as __decorate, t as ArbacUserProvider } from "../user.provider-BiM5P8fl.mjs";
|
|
2
|
+
import { t as AoothArbacUserCredentials } from "../user-Dzu_08g0.mjs";
|
|
3
3
|
import { defineWook, key } from "@wooksjs/event-core";
|
|
4
4
|
import { Injectable } from "moost";
|
|
5
|
-
//#region
|
|
5
|
+
//#region src/atscript/role-list.ts
|
|
6
|
+
/**
|
|
7
|
+
* Collect unique, non-empty strings from an iterable of unknown values,
|
|
8
|
+
* preserving first-seen order and dropping anything that is not a non-empty
|
|
9
|
+
* string. Shared by the user-side role extraction
|
|
10
|
+
* (`AtscriptArbacUserProvider.extractRoles`) and the credential-side
|
|
11
|
+
* attenuation role parse (`extractAttenuation`) so both apply identical
|
|
12
|
+
* dedup / drop-empty rules.
|
|
13
|
+
*/
|
|
14
|
+
function uniqueStrings(values) {
|
|
15
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const v of values) if (typeof v === "string" && v !== "" && !seen.has(v)) {
|
|
18
|
+
seen.add(v);
|
|
19
|
+
out.push(v);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region \0@oxc-project+runtime@0.133.0/helpers/esm/decorateMetadata.js
|
|
6
25
|
function __decorateMetadata(k, v) {
|
|
7
26
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
8
27
|
}
|
|
9
28
|
//#endregion
|
|
10
29
|
//#region src/atscript/auto-provider.ts
|
|
11
|
-
const specCache = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
const specCache$2 = /* @__PURE__ */ new WeakMap();
|
|
12
31
|
/**
|
|
13
32
|
* Resolve the identifier field on an object type using the three-step chain
|
|
14
33
|
* documented on `ArbacExtractSpec.userIdField`. Used for the user type AND
|
|
@@ -47,7 +66,7 @@ function resolveIdentifierField(type) {
|
|
|
47
66
|
return metaId;
|
|
48
67
|
}
|
|
49
68
|
function getArbacExtractSpec(type) {
|
|
50
|
-
const cached = specCache.get(type);
|
|
69
|
+
const cached = specCache$2.get(type);
|
|
51
70
|
if (cached !== void 0) return cached;
|
|
52
71
|
const spec = {
|
|
53
72
|
userIdField: void 0,
|
|
@@ -56,7 +75,7 @@ function getArbacExtractSpec(type) {
|
|
|
56
75
|
};
|
|
57
76
|
const def = type.type;
|
|
58
77
|
if (def.kind !== "object") {
|
|
59
|
-
specCache.set(type, spec);
|
|
78
|
+
specCache$2.set(type, spec);
|
|
60
79
|
return spec;
|
|
61
80
|
}
|
|
62
81
|
const roleCandidates = [];
|
|
@@ -87,7 +106,7 @@ function getArbacExtractSpec(type) {
|
|
|
87
106
|
};
|
|
88
107
|
}
|
|
89
108
|
spec.userIdField = resolveIdentifierField(type);
|
|
90
|
-
specCache.set(type, spec);
|
|
109
|
+
specCache$2.set(type, spec);
|
|
91
110
|
return spec;
|
|
92
111
|
}
|
|
93
112
|
/**
|
|
@@ -127,6 +146,8 @@ const useFetchCache = defineWook((ctx) => {
|
|
|
127
146
|
return cache;
|
|
128
147
|
});
|
|
129
148
|
let AtscriptArbacUserProvider = class AtscriptArbacUserProvider extends ArbacUserProvider {
|
|
149
|
+
userType;
|
|
150
|
+
table;
|
|
130
151
|
spec;
|
|
131
152
|
projection;
|
|
132
153
|
withClause;
|
|
@@ -167,22 +188,10 @@ let AtscriptArbacUserProvider = class AtscriptArbacUserProvider extends ArbacUse
|
|
|
167
188
|
if (!roleField) return [];
|
|
168
189
|
const raw = record[roleField.name];
|
|
169
190
|
if (raw === void 0 || raw === null) return [];
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
seen.add(val);
|
|
175
|
-
out.push(val);
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
if (roleField.shape === "inline") if (Array.isArray(raw)) for (const item of raw) push(item);
|
|
179
|
-
else push(raw);
|
|
180
|
-
else {
|
|
181
|
-
if (!Array.isArray(raw)) return out;
|
|
182
|
-
const idField = roleField.roleTargetIdField;
|
|
183
|
-
for (const item of raw) if (item && typeof item === "object") push(item[idField]);
|
|
184
|
-
}
|
|
185
|
-
return out;
|
|
191
|
+
if (roleField.shape === "inline") return uniqueStrings(Array.isArray(raw) ? raw : [raw]);
|
|
192
|
+
if (!Array.isArray(raw)) return [];
|
|
193
|
+
const idField = roleField.roleTargetIdField;
|
|
194
|
+
return uniqueStrings(raw.map((item) => item && typeof item === "object" ? item[idField] : void 0));
|
|
186
195
|
}
|
|
187
196
|
/**
|
|
188
197
|
* Override seam: `@arbac.attribute` fields keyed by prop name. Undefined
|
|
@@ -226,4 +235,162 @@ let AtscriptArbacUserProvider = class AtscriptArbacUserProvider extends ArbacUse
|
|
|
226
235
|
};
|
|
227
236
|
AtscriptArbacUserProvider = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [Object, Object])], AtscriptArbacUserProvider);
|
|
228
237
|
//#endregion
|
|
229
|
-
|
|
238
|
+
//#region src/atscript/attenuation-extract.ts
|
|
239
|
+
const specCache$1 = /* @__PURE__ */ new WeakMap();
|
|
240
|
+
/**
|
|
241
|
+
* Walk a credential model's `@arbac.attenuate.*` annotations into a cached
|
|
242
|
+
* {@link ArbacAttenuationSpec}. Mirrors the user-side `getArbacExtractSpec`:
|
|
243
|
+
* structural, computed once, fail-loud on an ambiguous (multiple) role field.
|
|
244
|
+
*/
|
|
245
|
+
function getArbacAttenuationSpec(credType) {
|
|
246
|
+
const cached = specCache$1.get(credType);
|
|
247
|
+
if (cached !== void 0) return cached;
|
|
248
|
+
const spec = {
|
|
249
|
+
roleField: void 0,
|
|
250
|
+
attrFields: []
|
|
251
|
+
};
|
|
252
|
+
const def = credType.type;
|
|
253
|
+
if (def.kind !== "object") {
|
|
254
|
+
specCache$1.set(credType, spec);
|
|
255
|
+
return spec;
|
|
256
|
+
}
|
|
257
|
+
const roleCandidates = [];
|
|
258
|
+
for (const [fieldName, fieldType] of def.props) {
|
|
259
|
+
const md = fieldType.metadata;
|
|
260
|
+
if (md.get("arbac.attenuate.role")) roleCandidates.push(fieldName);
|
|
261
|
+
const userAttr = md.get("arbac.attenuate.attr");
|
|
262
|
+
if (typeof userAttr === "string" && userAttr !== "") spec.attrFields.push({
|
|
263
|
+
field: fieldName,
|
|
264
|
+
userAttr
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
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.`);
|
|
268
|
+
if (roleCandidates.length === 1) spec.roleField = roleCandidates[0];
|
|
269
|
+
specCache$1.set(credType, spec);
|
|
270
|
+
return spec;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Boot-time cross-model check: every `@arbac.attenuate.attr "userAttr"` target
|
|
274
|
+
* must name a real key in the user model's `@arbac.attribute` keyspace
|
|
275
|
+
* (`validUserAttrs`), else a typo silently no-ops the narrowing. Throws on the
|
|
276
|
+
* first miss. Call once at provider construction; safe and cheap (the spec is
|
|
277
|
+
* cached). Soundness does NOT depend on this — the scope conjunction clips
|
|
278
|
+
* regardless — but it turns a silent misconfig into a loud boot failure.
|
|
279
|
+
*/
|
|
280
|
+
function validateAttenuationTargets(credType, validUserAttrs) {
|
|
281
|
+
const spec = getArbacAttenuationSpec(credType);
|
|
282
|
+
const valid = new Set(validUserAttrs);
|
|
283
|
+
for (const { field, userAttr } of spec.attrFields) if (!valid.has(userAttr)) throw new Error(`validateAttenuationTargets: @arbac.attenuate.attr "${userAttr}" on field "${field}" targets a user attribute that does not exist in the user model's @arbac.attribute keyspace (${[...valid].join(", ") || "<none>"}). Fix the annotation argument.`);
|
|
284
|
+
}
|
|
285
|
+
/** Lenient role parse: accept `string | string[]`, drop empty/non-string, dedupe. */
|
|
286
|
+
function parseRoles(raw) {
|
|
287
|
+
return uniqueStrings(Array.isArray(raw) ? raw : [raw]);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Build the restrict-only {@link AoothArbacClaims} for a credential `record`
|
|
291
|
+
* (its typed root fields — e.g. the auth context, which carries the credential
|
|
292
|
+
* payload flat) by reading the fields the credential model annotates. Designed
|
|
293
|
+
* to back a consumer's `ArbacUserProvider.getAttenuation()` override while
|
|
294
|
+
* arbac-moost stays auth-agnostic (the consumer supplies the record).
|
|
295
|
+
*
|
|
296
|
+
* Returns:
|
|
297
|
+
* - `undefined` when the model declares NO attenuation fields, OR a normal
|
|
298
|
+
* (non-attenuated) token carries none of them set → full user authority.
|
|
299
|
+
* - `{ roles, attrs }` otherwise. The narrowing is then clipped by the engine's
|
|
300
|
+
* scope conjunction, so it can never widen beyond the user.
|
|
301
|
+
*
|
|
302
|
+
* Fail-closed: when the assumed-role field IS set but holds a malformed value
|
|
303
|
+
* (no usable role strings), {@link parseRoles} yields `[]` — deny-all — never
|
|
304
|
+
* full authority. Absent (`undefined`/`null`) = the field was not set, so roles
|
|
305
|
+
* are omitted (attrs-only narrowing), which is the distinct "no role narrowing"
|
|
306
|
+
* case, not a malformed one.
|
|
307
|
+
*/
|
|
308
|
+
function extractAttenuation(credType, record) {
|
|
309
|
+
const spec = getArbacAttenuationSpec(credType);
|
|
310
|
+
if (spec.roleField === void 0 && spec.attrFields.length === 0) return void 0;
|
|
311
|
+
if (!record) return void 0;
|
|
312
|
+
const rec = record;
|
|
313
|
+
let roles;
|
|
314
|
+
if (spec.roleField !== void 0) {
|
|
315
|
+
const raw = rec[spec.roleField];
|
|
316
|
+
if (raw !== void 0 && raw !== null) roles = parseRoles(raw);
|
|
317
|
+
}
|
|
318
|
+
let attrs;
|
|
319
|
+
for (const { field, userAttr } of spec.attrFields) {
|
|
320
|
+
const value = rec[field];
|
|
321
|
+
if (value !== void 0 && value !== null) {
|
|
322
|
+
attrs ??= {};
|
|
323
|
+
attrs[userAttr] = value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (roles === void 0 && attrs === void 0) return void 0;
|
|
327
|
+
const out = {};
|
|
328
|
+
if (roles !== void 0) out.roles = roles;
|
|
329
|
+
if (attrs !== void 0) out.attrs = attrs;
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/atscript/handle-spec.ts
|
|
334
|
+
const specCache = /* @__PURE__ */ new WeakMap();
|
|
335
|
+
/** A `@db.index.unique` annotation is a non-empty `(string | true)[]`. */
|
|
336
|
+
function hasUniqueIndex(md) {
|
|
337
|
+
const unique = md.get("db.index.unique");
|
|
338
|
+
return Array.isArray(unique) && unique.length > 0;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Resolve a single handle field for one role: fail loud on ambiguity (more than
|
|
342
|
+
* one annotated field — there is no canonical choice), and warn-and-disable when
|
|
343
|
+
* the sole annotated field lacks a unique index.
|
|
344
|
+
*/
|
|
345
|
+
function resolveHandleField(role, annotation, candidates, warnings) {
|
|
346
|
+
if (candidates.length === 0) return void 0;
|
|
347
|
+
if (candidates.length > 1) throw new Error(`getAoothUserHandleSpec: multiple ${annotation} fields declared (${candidates.map((c) => c.field).join(", ")}). Exactly one ${role} login handle is supported — drop ${annotation} from all but the canonical field.`);
|
|
348
|
+
const { field, md } = candidates[0];
|
|
349
|
+
if (!hasUniqueIndex(md)) {
|
|
350
|
+
warnings.push(`${annotation} on field "${field}" has no @db.index.unique — a login handle must resolve to at most one row, so the ${role} handle is DISABLED (login/recovery by ${role} unavailable). Add @db.index.unique to the field to enable it.`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
return field;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Walk a user model's `@aooth.user.*` annotations into a cached
|
|
357
|
+
* {@link AoothUserHandleSpec}. Mirrors {@link getArbacAttenuationSpec}:
|
|
358
|
+
* structural, computed once, fail-loud on ambiguity. A model with no
|
|
359
|
+
* `@aooth.user.*` field resolves to all-`undefined` handles (email/phone login
|
|
360
|
+
* is simply unavailable — graceful degradation, not an error).
|
|
361
|
+
*/
|
|
362
|
+
function getAoothUserHandleSpec(userType) {
|
|
363
|
+
const cached = specCache.get(userType);
|
|
364
|
+
if (cached !== void 0) return cached;
|
|
365
|
+
const spec = {
|
|
366
|
+
emailField: void 0,
|
|
367
|
+
phoneField: void 0,
|
|
368
|
+
handleFields: [],
|
|
369
|
+
warnings: []
|
|
370
|
+
};
|
|
371
|
+
const def = userType.type;
|
|
372
|
+
if (def.kind !== "object") {
|
|
373
|
+
specCache.set(userType, spec);
|
|
374
|
+
return spec;
|
|
375
|
+
}
|
|
376
|
+
const emailCandidates = [];
|
|
377
|
+
const phoneCandidates = [];
|
|
378
|
+
for (const [fieldName, fieldType] of def.props) {
|
|
379
|
+
const md = fieldType.metadata;
|
|
380
|
+
if (md.get("aooth.user.email")) emailCandidates.push({
|
|
381
|
+
field: fieldName,
|
|
382
|
+
md
|
|
383
|
+
});
|
|
384
|
+
if (md.get("aooth.user.phone")) phoneCandidates.push({
|
|
385
|
+
field: fieldName,
|
|
386
|
+
md
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
spec.emailField = resolveHandleField("email", "@aooth.user.email", emailCandidates, spec.warnings);
|
|
390
|
+
spec.phoneField = resolveHandleField("phone", "@aooth.user.phone", phoneCandidates, spec.warnings);
|
|
391
|
+
spec.handleFields = [spec.emailField, spec.phoneField].filter((f) => f !== void 0);
|
|
392
|
+
specCache.set(userType, spec);
|
|
393
|
+
return spec;
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
export { AoothArbacUserCredentials, AtscriptArbacUserProvider, extractAttenuation, getAoothUserHandleSpec, getArbacAttenuationSpec, validateAttenuationTargets };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { n as ArbacUserProviderToken, t as ArbacUserProvider } from "./user.provider-
|
|
1
|
+
import { a as ArbacDbScope, c as enforceControlsPolicy, d as NavRelationKey, f as NavTarget, i as conjoinArbacDbScopes, l as extractUsedControlValues, m as ProjectionOf, n as ArbacUserProviderToken, o as AsArbacDbController, p as OwnFieldKey, r as AoothArbacClaims, s as applyAllowedFieldsAndSet, t as ArbacUserProvider, u as ControlsOf } from "./user.provider-CKPMp3G8.mjs";
|
|
2
2
|
import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
|
|
3
3
|
import { TAuthGuardDef } from "@moostjs/event-http";
|
|
4
4
|
import { EventContext } from "@wooksjs/event-core";
|
|
5
5
|
import { Mate, TMateParamMeta, TMoostMetadata } from "moost";
|
|
6
|
-
import { ControlGate,
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { TMetaResponse } from "@atscript/db";
|
|
6
|
+
import { ControlGate, TProjection } from "@aooth/arbac";
|
|
7
|
+
import { AsDbReadableController } from "@atscript/moost-db";
|
|
8
|
+
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
10
9
|
|
|
11
10
|
//#region src/arbac.composables.d.ts
|
|
12
11
|
interface ArbacBindings {
|
|
@@ -112,217 +111,6 @@ type ArbacMate = Mate<TMoostMetadata & {
|
|
|
112
111
|
*/
|
|
113
112
|
declare function getArbacMate(): ArbacMate;
|
|
114
113
|
//#endregion
|
|
115
|
-
//#region src/db/scope-types.d.ts
|
|
116
|
-
/**
|
|
117
|
-
* Unwrap a `NavPropsOf<T>[K]` value to its target model type:
|
|
118
|
-
* `Comment[]` → `Comment`, `Task` → `Task`. Used to recurse `with.<K>` into
|
|
119
|
-
* the joined model's own type so per-relation scopes get typed against the
|
|
120
|
-
* RIGHT entity (not the array wrapper).
|
|
121
|
-
*/
|
|
122
|
-
type NavTarget<U> = U extends readonly (infer V)[] ? V : U;
|
|
123
|
-
/**
|
|
124
|
-
* True only when `T` is `unknown` (or `any`). `unknown extends T` holds for
|
|
125
|
-
* both. Callers use this to pick the legacy untyped shape when no model is
|
|
126
|
-
* annotated.
|
|
127
|
-
*/
|
|
128
|
-
type IsUnknown<T> = unknown extends T ? true : false;
|
|
129
|
-
/**
|
|
130
|
-
* Permissive own-field key set. With a typed `T`, autocompletes against
|
|
131
|
-
* `keyof OwnPropsOf<T>` while still accepting dotted-path projections like
|
|
132
|
-
* `mfa.value` via the `(string & {})` escape hatch. With `T = unknown`,
|
|
133
|
-
* `OwnPropsOf<unknown>` collapses to `unknown` so `keyof` is `never` →
|
|
134
|
-
* falls back to plain `string`.
|
|
135
|
-
*/
|
|
136
|
-
type OwnFieldKey<T> = keyof OwnPropsOf<T> extends never ? string : (keyof OwnPropsOf<T> & string) | (string & {});
|
|
137
|
-
/**
|
|
138
|
-
* Same idiom for nav relation names. `NavPropsOf<unknown>` is
|
|
139
|
-
* `Record<string, never>`, so its `keyof` is `string` — the typed branch
|
|
140
|
-
* still resolves to plain `string` (the `(string & {})` escape collapses
|
|
141
|
-
* with `string`).
|
|
142
|
-
*/
|
|
143
|
-
type NavRelationKey<T> = keyof NavPropsOf<T> extends never ? string : (keyof NavPropsOf<T> & string) | (string & {});
|
|
144
|
-
/**
|
|
145
|
-
* Typed projection keyed by own-field paths (with dotted-path escape).
|
|
146
|
-
* Falls back to the legacy `TProjection = Record<string, 0 | 1>` for
|
|
147
|
-
* untyped scopes so existing callers that pass the result straight to
|
|
148
|
-
* `unionProjections` / `restrictProjection` keep compiling.
|
|
149
|
-
*/
|
|
150
|
-
type ProjectionOf<T> = IsUnknown<T> extends true ? TProjection : Partial<Record<OwnFieldKey<T>, 0 | 1>>;
|
|
151
|
-
/**
|
|
152
|
-
* Typed control gates: `$with` against nav names, `$select` against own
|
|
153
|
-
* fields, other `$`-prefixed controls passthrough. Falls back to the
|
|
154
|
-
* legacy untyped `Record<string, ControlGate>` for `T = unknown` so
|
|
155
|
-
* internal helpers (`unionControlsPolicy`) and existing untyped call
|
|
156
|
-
* sites keep compiling.
|
|
157
|
-
*/
|
|
158
|
-
type ControlsOf<T> = IsUnknown<T> extends true ? Record<string, ControlGate$1> : {
|
|
159
|
-
$with?: Array<NavRelationKey<T>> | boolean;
|
|
160
|
-
$select?: Array<OwnFieldKey<T>> | boolean;
|
|
161
|
-
$groupBy?: ControlGate$1;
|
|
162
|
-
$having?: ControlGate$1;
|
|
163
|
-
[key: `$${string}`]: ControlGate$1 | Array<string> | undefined;
|
|
164
|
-
};
|
|
165
|
-
//#endregion
|
|
166
|
-
//#region src/db/as-arbac-db-controller.d.ts
|
|
167
|
-
/**
|
|
168
|
-
* Contract returned by an ARBAC role's scope predicate on a DB-backed resource.
|
|
169
|
-
*
|
|
170
|
-
* Apps can extend this interface with their own fields via TypeScript
|
|
171
|
-
* declaration merging — both at role-definition time (returned from
|
|
172
|
-
* `role.allow(...)`) and at consumption time (custom controller overrides
|
|
173
|
-
* reading `scopes[i].myCustomField` get full type safety):
|
|
174
|
-
*
|
|
175
|
-
* @example
|
|
176
|
-
* ```ts
|
|
177
|
-
* declare module '@aooth/arbac-moost' {
|
|
178
|
-
* interface ArbacDbScope {
|
|
179
|
-
* tenantId?: string
|
|
180
|
-
* }
|
|
181
|
-
* }
|
|
182
|
-
* ```
|
|
183
|
-
*/
|
|
184
|
-
interface ArbacDbScope<T = unknown> {
|
|
185
|
-
filter?: TScopeFilter;
|
|
186
|
-
projection?: ProjectionOf<T>;
|
|
187
|
-
set?: Partial<Record<OwnFieldKey<T>, unknown>>;
|
|
188
|
-
allowedFields?: Array<OwnFieldKey<T>>;
|
|
189
|
-
/**
|
|
190
|
-
* Per-control gates for Uniquery URL controls (`$with`, `$groupBy`, `$having`, …).
|
|
191
|
-
* Evaluated by {@link AsArbacDbController.validateControls} before query execution;
|
|
192
|
-
* a violation throws `HttpError(403)`.
|
|
193
|
-
*
|
|
194
|
-
* Per-key semantics ({@link ControlGate}):
|
|
195
|
-
* - absent / `true` — allowed.
|
|
196
|
-
* - `false` — denied entirely.
|
|
197
|
-
* - `readonly string[]` — whitelist (e.g. `{ $with: ['comments'] }` allows
|
|
198
|
-
* `?$with=comments`, rejects `?$with=tasks`). Supported only for `$with`
|
|
199
|
-
* (relation names) and `$groupBy` (column names) in v1.
|
|
200
|
-
*
|
|
201
|
-
* Across roles, gates union additively (silence wins) via {@link unionControlsPolicy}.
|
|
202
|
-
*
|
|
203
|
-
* @example `{ controls: { $with: false } }` — disable $with for this role.
|
|
204
|
-
* @example `{ controls: { $with: ['comments', 'owner'] } }` — restrict relations.
|
|
205
|
-
*/
|
|
206
|
-
controls?: ControlsOf<T>;
|
|
207
|
-
/**
|
|
208
|
-
* Per-relation sub-scopes applied when the request expands a relation via
|
|
209
|
-
* `?$with=<name>`. Recursive — each sub-scope has the same shape and can
|
|
210
|
-
* declare its own `with` for nested expansions (e.g. tasks → comments → task).
|
|
211
|
-
*
|
|
212
|
-
* **Authority model**: the PARENT scope owns the policy for joined rows.
|
|
213
|
-
* arbac-moost does NOT re-evaluate ARBAC against the joined resource's own
|
|
214
|
-
* scopes — that would be a confusing indirection and a perf hit. Whatever
|
|
215
|
-
* the parent declares here is what surfaces from the expansion.
|
|
216
|
-
*
|
|
217
|
-
* **Union across roles**: when multiple roles allow the same parent table,
|
|
218
|
-
* their `with[name]` sub-scopes are unioned at every nested level using the
|
|
219
|
-
* existing `unionProjections` / `mergeScopeFilters` / `unionControlsPolicy`
|
|
220
|
-
* primitives (additive: broader access wins, same rules as the parent).
|
|
221
|
-
*
|
|
222
|
-
* **Silence wins**: if no role declares `with.<name>`, expansion is
|
|
223
|
-
* unrestricted (matches `controls.$with` whitelist semantics; the gate
|
|
224
|
-
* still applies if declared).
|
|
225
|
-
*/
|
|
226
|
-
with?: WithOf<T>;
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Per-relation sub-scope map. For `T = unknown` falls back to the legacy
|
|
230
|
-
* untyped `Record<string, ArbacDbScope>`. With a typed `T`, each known
|
|
231
|
-
* relation key gets its scope typed against the joined model (via
|
|
232
|
-
* `NavTarget` to unwrap arrays), while arbitrary `(string & {})` keys
|
|
233
|
-
* keep the untyped escape hatch. Lives here (not in `scope-types.ts`)
|
|
234
|
-
* because it must reference `ArbacDbScope` recursively.
|
|
235
|
-
*/
|
|
236
|
-
type WithOf<T> = unknown extends T ? Record<string, ArbacDbScope> : { [K in NavRelationKey<T>]?: K extends keyof NavPropsOf<T> ? ArbacDbScope<NavTarget<NavPropsOf<T>[K]>> : ArbacDbScope };
|
|
237
|
-
declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType> extends AsDbController<T> {
|
|
238
|
-
protected transformFilter(filter: Record<string, unknown> | undefined): Promise<Record<string, unknown>>;
|
|
239
|
-
protected transformProjection(projection?: TProjection): TProjection | undefined | Promise<TProjection | undefined>;
|
|
240
|
-
/**
|
|
241
|
-
* Translate `@uniqu/url` parser failures into HTTP 400 instead of letting them
|
|
242
|
-
* bubble as 500. The base `parseQueryString` calls `parseUrl()` directly with
|
|
243
|
-
* no try/catch, so a malformed `?status=...` (e.g. unquoted single quote at a
|
|
244
|
-
* non-string position) surfaces as a server error — misleading, since the
|
|
245
|
-
* server is fine and the client sent bad input.
|
|
246
|
-
*
|
|
247
|
-
* Narrow targeting: only `SyntaxError` raised from inside the parser is
|
|
248
|
-
* remapped. Other errors (e.g. a programmer-introduced TypeError in a future
|
|
249
|
-
* override) still bubble as 500. SQL-injection-shaped payloads remain safely
|
|
250
|
-
* handled either way — see SEC-17 for the parameterisation invariant; this
|
|
251
|
-
* override is an orthogonal robustness pin around the parse step.
|
|
252
|
-
*/
|
|
253
|
-
protected parseQueryString(url: string): ReturnType<AsDbController<T>["parseQueryString"]>;
|
|
254
|
-
/**
|
|
255
|
-
* Same parser-error → 400 remap as {@link parseQueryString}, applied to the
|
|
256
|
-
* `/one` and `/one?…` code paths which use `parseControlsOnlyFromUrl`.
|
|
257
|
-
*/
|
|
258
|
-
protected parseControlsOnlyFromUrl(url: string): ReturnType<AsDbController<T>["parseControlsOnlyFromUrl"]>;
|
|
259
|
-
/**
|
|
260
|
-
* Enforce per-role `ArbacDbScope.controls` gates against the parsed Uniquery
|
|
261
|
-
* controls of a request. Runs after the base validator (which checks the
|
|
262
|
-
* controls DTO shape) and BEFORE the query/aggregation pipeline executes.
|
|
263
|
-
*
|
|
264
|
-
* `transformFilter` runs first on every read endpoint and caches the
|
|
265
|
-
* evaluated scopes via `arbac.setScopes(...)`, so we read those scopes
|
|
266
|
-
* directly here without re-evaluating ARBAC.
|
|
267
|
-
*
|
|
268
|
-
* On a violation we throw `HttpError(403)`. moost-db's `query` / `pages` /
|
|
269
|
-
* `getOne` handlers do NOT wrap `validateParsed` in try/catch, so the
|
|
270
|
-
* thrown error bubbles to moost which translates it to a 403 response.
|
|
271
|
-
*/
|
|
272
|
-
protected validateControls(controls: Record<string, unknown>, type: "query" | "pages" | "getOne"): string | undefined;
|
|
273
|
-
protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
|
|
274
|
-
protected onWrite(action: "insert" | "insertMany" | "replace" | "replaceMany" | "update" | "updateMany", data: unknown): Promise<unknown>;
|
|
275
|
-
protected onRemove(id: unknown): Promise<unknown>;
|
|
276
|
-
private assertInScope;
|
|
277
|
-
private identifierFields;
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Test-friendly internal helper — exported for unit tests and helper
|
|
281
|
-
* composition; regular consumers should not call this directly.
|
|
282
|
-
*
|
|
283
|
-
* Enforce a per-control policy against a parsed Uniquery `controls` map.
|
|
284
|
-
*
|
|
285
|
-
* Throws `HttpError(403)` on the first violation. Pure (no DI) so it is
|
|
286
|
-
* trivially unit-testable; `validateControls` wires it up to the controller's
|
|
287
|
-
* cached scopes.
|
|
288
|
-
*
|
|
289
|
-
* Semantics per gate (see {@link ControlGate}):
|
|
290
|
-
* - `true` (or absent — dropped by `unionControlsPolicy`): allow.
|
|
291
|
-
* - `false`: deny if the control is used at all.
|
|
292
|
-
* - `readonly string[]`: allow only the listed values; reject any other.
|
|
293
|
-
*
|
|
294
|
-
* "Used" means the control key is present AND non-empty (an empty array is
|
|
295
|
-
* treated as not used, matching how the parser leaves missing controls).
|
|
296
|
-
*/
|
|
297
|
-
declare function enforceControlsPolicy(policy: Record<string, ControlGate$1>, controls: Record<string, unknown>): void;
|
|
298
|
-
/**
|
|
299
|
-
* Test-friendly internal helper — exported for unit tests and helper
|
|
300
|
-
* composition; regular consumers should not call this directly.
|
|
301
|
-
*
|
|
302
|
-
* Extract the set of "named values" from a Uniquery control payload, for
|
|
303
|
-
* use against a whitelist gate.
|
|
304
|
-
*
|
|
305
|
-
* Currently supported (matches `WHITELISTABLE_CONTROLS` in `unionControlsPolicy`):
|
|
306
|
-
* - `$with` — array of `{ name, … }` objects (per `TypedWithRelation`,
|
|
307
|
-
* see `@uniqu/core` parser at `parseWithSegment`); we extract `name`.
|
|
308
|
-
* Bare strings are tolerated for forward compatibility.
|
|
309
|
-
* - `$groupBy` — array of column names (strings). Returned as-is.
|
|
310
|
-
*
|
|
311
|
-
* For unknown controls we return an empty array; the caller then enforces
|
|
312
|
-
* `false`-only semantics (controlled by `unionControlsPolicy`'s whitelist
|
|
313
|
-
* gate, which throws if a non-whitelistable control receives a string[]).
|
|
314
|
-
*/
|
|
315
|
-
declare function extractUsedControlValues(key: string, value: unknown): string[];
|
|
316
|
-
/**
|
|
317
|
-
* Test-friendly internal helper — exported for unit tests and helper
|
|
318
|
-
* composition; regular consumers should not call this directly.
|
|
319
|
-
*
|
|
320
|
-
* Apply the union of `allowedFields` whitelists (with `preserveFields`
|
|
321
|
-
* always preserved) and overlay each scope's `set` overrides. Returns a
|
|
322
|
-
* shallow copy; original `data` is not mutated.
|
|
323
|
-
*/
|
|
324
|
-
declare function applyAllowedFieldsAndSet(data: unknown, scopes: ArbacDbScope[], preserveFields?: readonly string[]): unknown;
|
|
325
|
-
//#endregion
|
|
326
114
|
//#region src/db/as-arbac-db-readable-controller.d.ts
|
|
327
115
|
/**
|
|
328
116
|
* Read-only mirror of {@link AsArbacDbController} for view-style controllers
|
|
@@ -350,4 +138,4 @@ declare class AsArbacDbReadableController<T extends TAtscriptAnnotatedType = TAt
|
|
|
350
138
|
*/
|
|
351
139
|
declare class MoostArbac<TUserAttrs extends object, TScope extends object> extends Arbac$1<TUserAttrs, TScope> {}
|
|
352
140
|
//#endregion
|
|
353
|
-
export { Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, type ControlGate, type ControlsOf, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, applyAllowedFieldsAndSet, arbacAuthorizeInterceptor, arbacPatternToRegex, enforceControlsPolicy, extractUsedControlValues, getArbacMate, useArbac };
|
|
141
|
+
export { type AoothArbacClaims, Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, type ControlGate, type ControlsOf, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, applyAllowedFieldsAndSet, arbacAuthorizeInterceptor, arbacPatternToRegex, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, useArbac };
|