@vertesia/studio-utils 1.6.0-dev.20260918.115658Z → 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.
@@ -1,2 +1,3 @@
1
1
  export { matchConditions, matchLike, resolveConditions, resolvePath } from './match.js';
2
+ export { buildPrincipalContext, type MergedUserFields, type MergeSource, mergeUserFields, } from './principal-context.js';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -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"}
@@ -1,2 +1,3 @@
1
1
  export { matchConditions, matchLike, resolveConditions, resolvePath } from './match.js';
2
+ export { buildPrincipalContext, mergeUserFields, } from './principal-context.js';
2
3
  //# sourceMappingURL=index.js.map
@@ -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":"AAiCA;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,MAAqB,EAAE,IAAiB;IACpE,MAAM,WAAW,GAA4B,EAAE,CAAC;IAChD,IAAI,YAAgC,CAAC;IACrC,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;IAC1C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;QACrC,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YACpB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC3D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;oBACvB,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;oBAClC,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;wBAC1B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,QAAQ,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC;wBAC7C,WAAW,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;oBACvC,CAAC;yBAAM,CAAC;wBACJ,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;oBAClC,CAAC;gBACL,CAAC;qBAAM,CAAC;oBACJ,WAAW,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBAC7B,CAAC;YACL,CAAC;QACL,CAAC;QAED,0FAA0F;QAC1F,iFAAiF;QACjF,IAAI,MAAM,CAAC,SAAS,IAAI,IAAI,EAAE,CAAC;YAC3B,YAAY,GAAG,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC;QAC5G,CAAC;QAED,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;YACtB,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;gBAClC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC3B,CAAC;QACL,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YACd,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;gBAC1B,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACnB,CAAC;QACL,CAAC;IACL,CAAC;IAED,OAAO;QACH,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;QACzE,SAAS,EAAE,YAAY;QACvB,YAAY,EAAE,eAAe,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,SAAS;QAChF,IAAI,EAAE,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS;KAC3D,CAAC;AACN,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,qBAAqB,CACjC,IAAmB,EACnB,MAAmB,EACnB,KAAqB,EACrB,EAAW;IAEX,OAAO;QACH,IAAI;QACJ,EAAE,EAAE,EAAE,IAAI,EAAE;QACZ,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,CAAC;QAChC,YAAY,EAAE,MAAM,CAAC,YAAY,IAAI,EAAE;QACvC,KAAK,EAAE,KAAK,IAAI,EAAE;QAClB,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,EAAE;KACtC,CAAC;AACN,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertesia/studio-utils",
3
- "version": "1.6.0-dev.20260918.115658Z",
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.3",
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.0"
53
+ "vitest": "5.0.1"
54
54
  },
55
55
  "dependencies": {
56
- "@llumiverse/common": "1.6.0-dev.20260918.055633Z",
57
- "@vertesia/common": "1.6.0-dev.20260918.115658Z",
58
- "@vertesia/jst": "1.6.0-dev.20260918.115658Z",
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": "5961811f0fdaee3b4902a2230875a3325e64ad80",
61
+ "gitHead": "8732631fd8099bdcab4f5375da2309f3cff34e19",
62
62
  "scripts": {
63
63
  "lint": "biome lint src",
64
64
  "test": "vitest run",
@@ -1 +1,7 @@
1
1
  export { matchConditions, matchLike, resolveConditions, resolvePath } from './match.js';
2
+ export {
3
+ buildPrincipalContext,
4
+ type MergedUserFields,
5
+ type MergeSource,
6
+ mergeUserFields,
7
+ } from './principal-context.js';
@@ -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
+ }