@vertesia/studio-utils 1.6.0-dev.20260918.090710Z → 1.6.0-dev.20260923.085325Z
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/lib/conditions/index.d.ts +1 -0
- package/lib/conditions/index.d.ts.map +1 -1
- package/lib/conditions/index.js +1 -0
- package/lib/conditions/index.js.map +1 -1
- package/lib/conditions/principal-context.d.ts +55 -0
- package/lib/conditions/principal-context.d.ts.map +1 -0
- package/lib/conditions/principal-context.js +81 -0
- package/lib/conditions/principal-context.js.map +1 -0
- package/package.json +7 -7
- package/src/conditions/index.ts +6 -0
- package/src/conditions/principal-context.ts +122 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conditions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conditions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACxF,OAAO,EACH,qBAAqB,EACrB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,eAAe,GAClB,MAAM,wBAAwB,CAAC"}
|
package/lib/conditions/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/conditions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/conditions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACxF,OAAO,EACH,qBAAqB,EAGrB,eAAe,GAClB,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AbacPrincipalContext, PrincipalKind } from '@vertesia/common';
|
|
2
|
+
/**
|
|
3
|
+
* The shared principal-context builder, consumed by both the token server (JWT `$principal.*`
|
|
4
|
+
* resolution at mint time) and studio-server (the `/iam/users/identity` endpoint). It is the single
|
|
5
|
+
* home for logic that used to be duplicated in `apps/token-server` and `apps/studio-server` and kept
|
|
6
|
+
* in sync by hand; the produced {@link AbacPrincipalContext} is exactly the shape ABAC rules resolve
|
|
7
|
+
* against.
|
|
8
|
+
*
|
|
9
|
+
* Browser-safe (pure, no Node built-ins): the UI condition editor lives downstream of the same
|
|
10
|
+
* `@vertesia/common` shape, and this package must stay loadable in the browser bundle.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* A user, group, or api-key record as it comes out of Mongo, where every optional field can be null
|
|
14
|
+
* as well as absent — which the inferred persistence types say out loud. The merge treats null and
|
|
15
|
+
* absent identically, so nothing about the resulting context changes; what changes is that the
|
|
16
|
+
* nullability is handled rather than asserted away at the call site.
|
|
17
|
+
*/
|
|
18
|
+
export interface MergeSource {
|
|
19
|
+
properties?: Record<string, unknown> | null;
|
|
20
|
+
clearance?: number | null;
|
|
21
|
+
compartments?: string[] | null;
|
|
22
|
+
tags?: string[] | null;
|
|
23
|
+
}
|
|
24
|
+
export interface MergedUserFields {
|
|
25
|
+
properties?: Record<string, unknown>;
|
|
26
|
+
clearance?: number;
|
|
27
|
+
compartments?: string[];
|
|
28
|
+
tags?: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Merge properties, clearance, compartments, and tags from groups + the principal.
|
|
32
|
+
*
|
|
33
|
+
* - properties: array values are deduplicated union, scalars use last-write-wins (principal wins)
|
|
34
|
+
* - clearance: max across all defined values (most permissive wins)
|
|
35
|
+
* - compartments / tags: deduplicated union of all arrays
|
|
36
|
+
*
|
|
37
|
+
* Used for the user token path (user + covering groups). The api-key path has no groups to merge and
|
|
38
|
+
* passes the key document straight to {@link buildPrincipalContext} as `fields`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function mergeUserFields(groups: MergeSource[], user: MergeSource): MergedUserFields;
|
|
41
|
+
/**
|
|
42
|
+
* Build the {@link AbacPrincipalContext} consumed by ResourceSet/PrincipalSet condition evaluation
|
|
43
|
+
* and mirrored into the JWT for runtime `$principal.*` resolution.
|
|
44
|
+
*
|
|
45
|
+
* - user: pass `mergeUserFields(groups, user)` as fields, the user's email, and the user id
|
|
46
|
+
* - apikey: pass the api key document itself (it carries optional properties/clearance/compartments)
|
|
47
|
+
* and the api key id
|
|
48
|
+
*
|
|
49
|
+
* Defaults are applied so condition operators have well-defined behaviour when the principal has no
|
|
50
|
+
* merged value: clearance → 0, compartments → [], tags → [], properties → {}, id → '', email → ''.
|
|
51
|
+
* `email` is published optional on the wire but defaulted to '' here so `$principal.email` resolves
|
|
52
|
+
* to a string rather than undefined during rule evaluation.
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildPrincipalContext(kind: PrincipalKind, fields: MergeSource, email?: string | null, id?: string): AbacPrincipalContext;
|
|
55
|
+
//# sourceMappingURL=principal-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"principal-context.d.ts","sourceRoot":"","sources":["../../src/conditions/principal-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAE5E;;;;;;;;;GASG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,WAAW,GAAG,gBAAgB,CAgD1F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACjC,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,EACrB,EAAE,CAAC,EAAE,MAAM,GACZ,oBAAoB,CAUtB"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge properties, clearance, compartments, and tags from groups + the principal.
|
|
3
|
+
*
|
|
4
|
+
* - properties: array values are deduplicated union, scalars use last-write-wins (principal wins)
|
|
5
|
+
* - clearance: max across all defined values (most permissive wins)
|
|
6
|
+
* - compartments / tags: deduplicated union of all arrays
|
|
7
|
+
*
|
|
8
|
+
* Used for the user token path (user + covering groups). The api-key path has no groups to merge and
|
|
9
|
+
* passes the key document straight to {@link buildPrincipalContext} as `fields`.
|
|
10
|
+
*/
|
|
11
|
+
export function mergeUserFields(groups, user) {
|
|
12
|
+
const mergedProps = {};
|
|
13
|
+
let maxClearance;
|
|
14
|
+
const allCompartments = new Set();
|
|
15
|
+
const allTags = new Set();
|
|
16
|
+
for (const source of [...groups, user]) {
|
|
17
|
+
if (source.properties) {
|
|
18
|
+
for (const [key, value] of Object.entries(source.properties)) {
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
const existing = mergedProps[key];
|
|
21
|
+
if (Array.isArray(existing)) {
|
|
22
|
+
const set = new Set([...existing, ...value]);
|
|
23
|
+
mergedProps[key] = Array.from(set);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
mergedProps[key] = [...value];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
mergedProps[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// `!= null` rather than `!== undefined`: a stored null means "no clearance recorded", the
|
|
35
|
+
// same as an absent field, and must not reach Math.max where it would read as 0.
|
|
36
|
+
if (source.clearance != null) {
|
|
37
|
+
maxClearance = maxClearance !== undefined ? Math.max(maxClearance, source.clearance) : source.clearance;
|
|
38
|
+
}
|
|
39
|
+
if (source.compartments) {
|
|
40
|
+
for (const c of source.compartments) {
|
|
41
|
+
allCompartments.add(c);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (source.tags) {
|
|
45
|
+
for (const t of source.tags) {
|
|
46
|
+
allTags.add(t);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
properties: Object.keys(mergedProps).length > 0 ? mergedProps : undefined,
|
|
52
|
+
clearance: maxClearance,
|
|
53
|
+
compartments: allCompartments.size > 0 ? Array.from(allCompartments) : undefined,
|
|
54
|
+
tags: allTags.size > 0 ? Array.from(allTags) : undefined,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build the {@link AbacPrincipalContext} consumed by ResourceSet/PrincipalSet condition evaluation
|
|
59
|
+
* and mirrored into the JWT for runtime `$principal.*` resolution.
|
|
60
|
+
*
|
|
61
|
+
* - user: pass `mergeUserFields(groups, user)` as fields, the user's email, and the user id
|
|
62
|
+
* - apikey: pass the api key document itself (it carries optional properties/clearance/compartments)
|
|
63
|
+
* and the api key id
|
|
64
|
+
*
|
|
65
|
+
* Defaults are applied so condition operators have well-defined behaviour when the principal has no
|
|
66
|
+
* merged value: clearance → 0, compartments → [], tags → [], properties → {}, id → '', email → ''.
|
|
67
|
+
* `email` is published optional on the wire but defaulted to '' here so `$principal.email` resolves
|
|
68
|
+
* to a string rather than undefined during rule evaluation.
|
|
69
|
+
*/
|
|
70
|
+
export function buildPrincipalContext(kind, fields, email, id) {
|
|
71
|
+
return {
|
|
72
|
+
kind,
|
|
73
|
+
id: id ?? '',
|
|
74
|
+
clearance: fields.clearance ?? 0,
|
|
75
|
+
compartments: fields.compartments ?? [],
|
|
76
|
+
email: email ?? '',
|
|
77
|
+
tags: fields.tags ?? [],
|
|
78
|
+
properties: fields.properties ?? {},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=principal-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"principal-context.js","sourceRoot":"","sources":["../../src/conditions/principal-context.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertesia/studio-utils",
|
|
3
|
-
"version": "1.6.0-dev.
|
|
3
|
+
"version": "1.6.0-dev.20260923.085325Z",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared utilities for Vertesia studio: prompt template rendering, schema-based mock data generation, prompt and interaction validation helpers.",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -46,19 +46,19 @@
|
|
|
46
46
|
"validation"
|
|
47
47
|
],
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^24.13.
|
|
49
|
+
"@types/node": "^24.13.5",
|
|
50
50
|
"@vertesia/tsconfig": "0.1.0",
|
|
51
51
|
"rolldown": "1.2.8",
|
|
52
52
|
"typescript": "7.0.2",
|
|
53
|
-
"vitest": "5.0.
|
|
53
|
+
"vitest": "5.0.1"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@llumiverse/common": "1.6.0-dev.
|
|
57
|
-
"@vertesia/common": "1.6.0-dev.
|
|
58
|
-
"@vertesia/jst": "1.6.0-dev.
|
|
56
|
+
"@llumiverse/common": "1.6.0-dev.20260923.085100Z",
|
|
57
|
+
"@vertesia/common": "1.6.0-dev.20260923.085325Z",
|
|
58
|
+
"@vertesia/jst": "1.6.0-dev.20260923.085325Z",
|
|
59
59
|
"handlebars": "^4.7.9"
|
|
60
60
|
},
|
|
61
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "8732631fd8099bdcab4f5375da2309f3cff34e19",
|
|
62
62
|
"scripts": {
|
|
63
63
|
"lint": "biome lint src",
|
|
64
64
|
"test": "vitest run",
|
package/src/conditions/index.ts
CHANGED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { AbacPrincipalContext, PrincipalKind } from '@vertesia/common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The shared principal-context builder, consumed by both the token server (JWT `$principal.*`
|
|
5
|
+
* resolution at mint time) and studio-server (the `/iam/users/identity` endpoint). It is the single
|
|
6
|
+
* home for logic that used to be duplicated in `apps/token-server` and `apps/studio-server` and kept
|
|
7
|
+
* in sync by hand; the produced {@link AbacPrincipalContext} is exactly the shape ABAC rules resolve
|
|
8
|
+
* against.
|
|
9
|
+
*
|
|
10
|
+
* Browser-safe (pure, no Node built-ins): the UI condition editor lives downstream of the same
|
|
11
|
+
* `@vertesia/common` shape, and this package must stay loadable in the browser bundle.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A user, group, or api-key record as it comes out of Mongo, where every optional field can be null
|
|
16
|
+
* as well as absent — which the inferred persistence types say out loud. The merge treats null and
|
|
17
|
+
* absent identically, so nothing about the resulting context changes; what changes is that the
|
|
18
|
+
* nullability is handled rather than asserted away at the call site.
|
|
19
|
+
*/
|
|
20
|
+
export interface MergeSource {
|
|
21
|
+
properties?: Record<string, unknown> | null;
|
|
22
|
+
clearance?: number | null;
|
|
23
|
+
compartments?: string[] | null;
|
|
24
|
+
tags?: string[] | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MergedUserFields {
|
|
28
|
+
properties?: Record<string, unknown>;
|
|
29
|
+
clearance?: number;
|
|
30
|
+
compartments?: string[];
|
|
31
|
+
tags?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Merge properties, clearance, compartments, and tags from groups + the principal.
|
|
36
|
+
*
|
|
37
|
+
* - properties: array values are deduplicated union, scalars use last-write-wins (principal wins)
|
|
38
|
+
* - clearance: max across all defined values (most permissive wins)
|
|
39
|
+
* - compartments / tags: deduplicated union of all arrays
|
|
40
|
+
*
|
|
41
|
+
* Used for the user token path (user + covering groups). The api-key path has no groups to merge and
|
|
42
|
+
* passes the key document straight to {@link buildPrincipalContext} as `fields`.
|
|
43
|
+
*/
|
|
44
|
+
export function mergeUserFields(groups: MergeSource[], user: MergeSource): MergedUserFields {
|
|
45
|
+
const mergedProps: Record<string, unknown> = {};
|
|
46
|
+
let maxClearance: number | undefined;
|
|
47
|
+
const allCompartments = new Set<string>();
|
|
48
|
+
const allTags = new Set<string>();
|
|
49
|
+
|
|
50
|
+
for (const source of [...groups, user]) {
|
|
51
|
+
if (source.properties) {
|
|
52
|
+
for (const [key, value] of Object.entries(source.properties)) {
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
const existing = mergedProps[key];
|
|
55
|
+
if (Array.isArray(existing)) {
|
|
56
|
+
const set = new Set([...existing, ...value]);
|
|
57
|
+
mergedProps[key] = Array.from(set);
|
|
58
|
+
} else {
|
|
59
|
+
mergedProps[key] = [...value];
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
mergedProps[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// `!= null` rather than `!== undefined`: a stored null means "no clearance recorded", the
|
|
68
|
+
// same as an absent field, and must not reach Math.max where it would read as 0.
|
|
69
|
+
if (source.clearance != null) {
|
|
70
|
+
maxClearance = maxClearance !== undefined ? Math.max(maxClearance, source.clearance) : source.clearance;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (source.compartments) {
|
|
74
|
+
for (const c of source.compartments) {
|
|
75
|
+
allCompartments.add(c);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (source.tags) {
|
|
80
|
+
for (const t of source.tags) {
|
|
81
|
+
allTags.add(t);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
properties: Object.keys(mergedProps).length > 0 ? mergedProps : undefined,
|
|
88
|
+
clearance: maxClearance,
|
|
89
|
+
compartments: allCompartments.size > 0 ? Array.from(allCompartments) : undefined,
|
|
90
|
+
tags: allTags.size > 0 ? Array.from(allTags) : undefined,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build the {@link AbacPrincipalContext} consumed by ResourceSet/PrincipalSet condition evaluation
|
|
96
|
+
* and mirrored into the JWT for runtime `$principal.*` resolution.
|
|
97
|
+
*
|
|
98
|
+
* - user: pass `mergeUserFields(groups, user)` as fields, the user's email, and the user id
|
|
99
|
+
* - apikey: pass the api key document itself (it carries optional properties/clearance/compartments)
|
|
100
|
+
* and the api key id
|
|
101
|
+
*
|
|
102
|
+
* Defaults are applied so condition operators have well-defined behaviour when the principal has no
|
|
103
|
+
* merged value: clearance → 0, compartments → [], tags → [], properties → {}, id → '', email → ''.
|
|
104
|
+
* `email` is published optional on the wire but defaulted to '' here so `$principal.email` resolves
|
|
105
|
+
* to a string rather than undefined during rule evaluation.
|
|
106
|
+
*/
|
|
107
|
+
export function buildPrincipalContext(
|
|
108
|
+
kind: PrincipalKind,
|
|
109
|
+
fields: MergeSource,
|
|
110
|
+
email?: string | null,
|
|
111
|
+
id?: string,
|
|
112
|
+
): AbacPrincipalContext {
|
|
113
|
+
return {
|
|
114
|
+
kind,
|
|
115
|
+
id: id ?? '',
|
|
116
|
+
clearance: fields.clearance ?? 0,
|
|
117
|
+
compartments: fields.compartments ?? [],
|
|
118
|
+
email: email ?? '',
|
|
119
|
+
tags: fields.tags ?? [],
|
|
120
|
+
properties: fields.properties ?? {},
|
|
121
|
+
};
|
|
122
|
+
}
|