@happyvertical/smrt-core 0.55.12 → 0.55.13
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/agents/cookbook.md +12 -3
- package/agents/object-runtime.md +11 -0
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +4 -1
- package/dist/collection.js.map +1 -1
- package/dist/cookbook/engine/document.d.ts.map +1 -1
- package/dist/cookbook/engine/document.js +16 -18
- package/dist/cookbook/engine/document.js.map +1 -1
- package/dist/cookbook/engine/types.d.ts +2 -0
- package/dist/cookbook/engine/types.d.ts.map +1 -1
- package/dist/cookbook/engine/types.js.map +1 -1
- package/dist/cookbook/v1.schema.json +60 -3
- package/dist/cookbook/validate.d.ts +40 -4
- package/dist/cookbook/validate.d.ts.map +1 -1
- package/dist/cookbook/validate.js +228 -58
- package/dist/cookbook/validate.js.map +1 -1
- package/dist/dispatch/bus.d.ts +9 -4
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +11 -6
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/dispatch/collections/Dispatches.d.ts +15 -5
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
- package/dist/dispatch/collections/Dispatches.js +29 -9
- package/dist/dispatch/collections/Dispatches.js.map +1 -1
- package/dist/dispatch/types.d.ts +5 -1
- package/dist/dispatch/types.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +5 -1
- package/dist/object.js.map +1 -1
- package/dist/smrt-knowledge.json +7 -7
- package/dist/utils.d.ts +21 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +52 -2
- package/dist/utils.js.map +1 -1
- package/package.json +10 -10
|
@@ -19,30 +19,52 @@ var SCHEMES = /* @__PURE__ */ new Set([
|
|
|
19
19
|
"dark",
|
|
20
20
|
"system"
|
|
21
21
|
]);
|
|
22
|
+
var SHELL_SLOTS = /* @__PURE__ */ new Set([
|
|
23
|
+
"header.start",
|
|
24
|
+
"header.center",
|
|
25
|
+
"header.end",
|
|
26
|
+
"footer.start",
|
|
27
|
+
"footer.center",
|
|
28
|
+
"footer.end",
|
|
29
|
+
"leftSidebar.header",
|
|
30
|
+
"leftSidebar.footer",
|
|
31
|
+
"rightSidebar.header",
|
|
32
|
+
"rightSidebar.footer"
|
|
33
|
+
]);
|
|
22
34
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
23
35
|
var isStrings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
24
36
|
var isStringRecord = (v, valid) => isObject(v) && !Object.hasOwn(v, "__proto__") && Object.values(v).every(valid);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
var add = (diagnostics, severity, code, path, message) => {
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
severity,
|
|
40
|
+
code,
|
|
41
|
+
path,
|
|
42
|
+
message
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
var error = (diagnostics, code, path, message) => add(diagnostics, "error", code, path, message);
|
|
46
|
+
var warning = (diagnostics, code, path, message) => add(diagnostics, "warning", code, path, message);
|
|
47
|
+
function checkPolicy(value, at, diagnostics) {
|
|
48
|
+
if (!isObject(value)) return error(diagnostics, "invalid_type", at, `${at} must be an object`);
|
|
49
|
+
for (const key of ["objectRef", "fieldName"]) if (typeof value[key] !== "string" || !value[key]) error(diagnostics, "invalid_value", `${at}.${key}`, `${at}.${key} must be a non-empty string`);
|
|
50
|
+
if (value.scopeType !== "app") error(diagnostics, "invalid_value", `${at}.scopeType`, `${at}.scopeType must be "app"`);
|
|
29
51
|
for (const key of [
|
|
30
52
|
"defaultValue",
|
|
31
53
|
"help",
|
|
32
54
|
"label"
|
|
33
55
|
]) {
|
|
34
56
|
const v = value[key];
|
|
35
|
-
if (v !== void 0 && v !== null && typeof v !== "string")
|
|
57
|
+
if (v !== void 0 && v !== null && typeof v !== "string") error(diagnostics, "invalid_type", `${at}.${key}`, `${at}.${key} must be a string or null`);
|
|
36
58
|
}
|
|
37
59
|
const vis = value.visibility;
|
|
38
|
-
if (vis != null && !(typeof vis === "string" && VISIBILITIES.has(vis)))
|
|
60
|
+
if (vis != null && !(typeof vis === "string" && VISIBILITIES.has(vis))) error(diagnostics, "invalid_value", `${at}.visibility`, `${at}.visibility must be basic, advanced or hidden`);
|
|
39
61
|
const order = value.displayOrder;
|
|
40
|
-
if (order != null && !(typeof order === "number" && Number.isFinite(order)))
|
|
41
|
-
if (value.locked != null && typeof value.locked !== "boolean")
|
|
62
|
+
if (order != null && !(typeof order === "number" && Number.isFinite(order))) error(diagnostics, "invalid_value", `${at}.displayOrder`, `${at}.displayOrder must be a finite number`);
|
|
63
|
+
if (value.locked != null && typeof value.locked !== "boolean") error(diagnostics, "invalid_type", `${at}.locked`, `${at}.locked must be a boolean`);
|
|
42
64
|
}
|
|
43
|
-
function checkLayout(value,
|
|
44
|
-
if (!isObject(value) || value.version !== 1) return
|
|
45
|
-
for (const key of ["sectionOrder", "hidden"]) if (value[key] !== void 0 && !isStrings(value[key]))
|
|
65
|
+
function checkLayout(value, diagnostics) {
|
|
66
|
+
if (!isObject(value) || value.version !== 1) return error(diagnostics, "invalid_value", "layout", "layout must be an object with version 1");
|
|
67
|
+
for (const key of ["sectionOrder", "hidden"]) if (value[key] !== void 0 && !isStrings(value[key])) error(diagnostics, "invalid_type", `layout.${key}`, `layout.${key} must be a list of strings`);
|
|
46
68
|
for (const key of [
|
|
47
69
|
"itemOrder",
|
|
48
70
|
"moved",
|
|
@@ -50,33 +72,99 @@ function checkLayout(value, errors) {
|
|
|
50
72
|
]) {
|
|
51
73
|
const v = value[key];
|
|
52
74
|
if (v === void 0) continue;
|
|
53
|
-
if (!(key === "itemOrder" ? isStringRecord(v, isStrings) : isStringRecord(v, (x) => typeof x === "string")))
|
|
75
|
+
if (!(key === "itemOrder" ? isStringRecord(v, isStrings) : isStringRecord(v, (x) => typeof x === "string"))) error(diagnostics, "invalid_type", `layout.${key}`, key === "itemOrder" ? "layout.itemOrder must map ids to lists of strings" : `layout.${key} must map ids to strings`);
|
|
76
|
+
}
|
|
77
|
+
if (isObject(value.placements)) {
|
|
78
|
+
for (const [id, slot] of Object.entries(value.placements)) if (typeof slot === "string" && !SHELL_SLOTS.has(slot)) error(diagnostics, "unknown_layout_slot", `layout.placements.${id}`, `Unknown shell slot "${slot}".`);
|
|
54
79
|
}
|
|
55
80
|
for (const key of [
|
|
56
81
|
"sections",
|
|
57
82
|
"items",
|
|
58
83
|
"panels"
|
|
59
|
-
]) if (value[key] !== void 0 && !isStringRecord(value[key], (x) => isObject(x)))
|
|
84
|
+
]) if (value[key] !== void 0 && !isStringRecord(value[key], (x) => isObject(x))) error(diagnostics, "invalid_type", `layout.${key}`, `layout.${key} must map ids to objects`);
|
|
60
85
|
const custom = value.customSections;
|
|
61
|
-
if (custom !== void 0 && !(Array.isArray(custom) && custom.every((c) => isObject(c) && typeof c.id === "string" && typeof c.label === "string")))
|
|
86
|
+
if (custom !== void 0 && !(Array.isArray(custom) && custom.every((c) => isObject(c) && typeof c.id === "string" && typeof c.label === "string"))) error(diagnostics, "invalid_type", "layout.customSections", "layout.customSections must be a list of { id, label }");
|
|
62
87
|
}
|
|
63
|
-
function checkTheme(value,
|
|
64
|
-
if (!isObject(value)) return
|
|
65
|
-
if (value.preset !== void 0 && typeof value.preset !== "string")
|
|
66
|
-
if (value.colorScheme !== void 0 && !(typeof value.colorScheme === "string" && SCHEMES.has(value.colorScheme)))
|
|
88
|
+
function checkTheme(value, diagnostics) {
|
|
89
|
+
if (!isObject(value)) return error(diagnostics, "invalid_type", "theme", "theme must be an object");
|
|
90
|
+
if (value.preset !== void 0 && typeof value.preset !== "string") error(diagnostics, "invalid_type", "theme.preset", "theme.preset must be a string");
|
|
91
|
+
if (value.colorScheme !== void 0 && !(typeof value.colorScheme === "string" && SCHEMES.has(value.colorScheme))) error(diagnostics, "invalid_value", "theme.colorScheme", "theme.colorScheme must be light, dark or system");
|
|
67
92
|
const custom = value.custom;
|
|
68
93
|
if (custom !== void 0) {
|
|
69
|
-
if (!isObject(custom) || typeof custom.primary !== "string")
|
|
70
|
-
else if (!/^#[0-9a-fA-F]{6}$/.test(custom.primary))
|
|
71
|
-
else if (custom.fontFamily !== void 0 && typeof custom.fontFamily !== "string")
|
|
94
|
+
if (!isObject(custom) || typeof custom.primary !== "string") error(diagnostics, "invalid_value", "theme.custom", "theme.custom must be an object with a string primary");
|
|
95
|
+
else if (!/^#[0-9a-fA-F]{6}$/.test(custom.primary)) error(diagnostics, "invalid_value", "theme.custom.primary", "theme.custom.primary must be a hex colour like #c2410c");
|
|
96
|
+
else if (custom.fontFamily !== void 0 && typeof custom.fontFamily !== "string") error(diagnostics, "invalid_type", "theme.custom.fontFamily", "theme.custom.fontFamily must be a string");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
var OVERVIEW_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/;
|
|
100
|
+
var WIDGET_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
101
|
+
var WIDGET_TYPE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
102
|
+
var isOptionValue = (value) => value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value) || typeof value === "boolean";
|
|
103
|
+
function normalizeOverviewWidget(value, at, diagnostics) {
|
|
104
|
+
if (!isObject(value)) {
|
|
105
|
+
warning(diagnostics, "malformed_overview_widget", at, `${at} was dropped`);
|
|
106
|
+
return null;
|
|
72
107
|
}
|
|
108
|
+
if ([
|
|
109
|
+
typeof value.id === "string" && WIDGET_ID.test(value.id),
|
|
110
|
+
typeof value.type === "string" && WIDGET_TYPE.test(value.type),
|
|
111
|
+
typeof value.span === "number" && Number.isInteger(value.span) && value.span >= 1 && value.span <= 4,
|
|
112
|
+
isObject(value.options) && Object.values(value.options).every(isOptionValue),
|
|
113
|
+
value.version === void 0 || typeof value.version === "number" && Number.isInteger(value.version) && value.version >= 1
|
|
114
|
+
].every(Boolean)) return value;
|
|
115
|
+
warning(diagnostics, "malformed_overview_widget", at, `${at} was dropped because it is not a valid overview widget`);
|
|
116
|
+
return null;
|
|
73
117
|
}
|
|
74
|
-
function
|
|
75
|
-
if (!isObject(value)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (value.
|
|
118
|
+
function normalizeOverviewPatch(value, at, diagnostics) {
|
|
119
|
+
if (!isObject(value)) {
|
|
120
|
+
warning(diagnostics, "malformed_overview_change", at, `${at} was dropped`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if ((value.span === void 0 || typeof value.span === "number" && Number.isInteger(value.span) && value.span >= 1 && value.span <= 4) && (value.options === void 0 || isObject(value.options) && Object.values(value.options).every(isOptionValue)) && (value.version === void 0 || typeof value.version === "number" && Number.isInteger(value.version) && value.version >= 1)) return value;
|
|
124
|
+
warning(diagnostics, "malformed_overview_change", at, `${at} was dropped because it is not a valid widget change`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function normalizeOverview(value, at, diagnostics) {
|
|
128
|
+
if (!isObject(value) || value.version !== 1) {
|
|
129
|
+
warning(diagnostics, "malformed_overview", at, `${at} was dropped because it must be an object with version 1`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const override = {
|
|
133
|
+
...value,
|
|
134
|
+
version: 1
|
|
135
|
+
};
|
|
136
|
+
for (const key of ["order", "removed"]) if (value[key] !== void 0 && !isStrings(value[key])) {
|
|
137
|
+
warning(diagnostics, "malformed_overview_list", `${at}.${key}`, `${at}.${key} was dropped because it must be a list of strings`);
|
|
138
|
+
delete override[key];
|
|
139
|
+
} else if (value[key] !== void 0) override[key] = [...new Set(value[key])];
|
|
140
|
+
if (value.added !== void 0) if (!Array.isArray(value.added)) {
|
|
141
|
+
warning(diagnostics, "malformed_overview_widgets", `${at}.added`, `${at}.added was dropped because it must be a list of widgets`);
|
|
142
|
+
delete override.added;
|
|
143
|
+
} else {
|
|
144
|
+
const added = value.added.flatMap((widget, index) => {
|
|
145
|
+
const normalized = normalizeOverviewWidget(widget, `${at}.added[${index}]`, diagnostics);
|
|
146
|
+
return normalized ? [normalized] : [];
|
|
147
|
+
});
|
|
148
|
+
if (added.length) override.added = added;
|
|
149
|
+
else delete override.added;
|
|
150
|
+
}
|
|
151
|
+
if (value.changed !== void 0) if (!isObject(value.changed) || Object.hasOwn(value.changed, "__proto__")) {
|
|
152
|
+
warning(diagnostics, "malformed_overview_changes", `${at}.changed`, `${at}.changed was dropped because it must map widget ids to objects`);
|
|
153
|
+
delete override.changed;
|
|
154
|
+
} else {
|
|
155
|
+
const changed = {};
|
|
156
|
+
for (const [id, patch] of Object.entries(value.changed)) {
|
|
157
|
+
if (!WIDGET_ID.test(id)) {
|
|
158
|
+
warning(diagnostics, "malformed_overview_change", `${at}.changed.${id}`, `${at}.changed.${id} was dropped because its widget id is invalid`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const normalized = normalizeOverviewPatch(patch, `${at}.changed.${id}`, diagnostics);
|
|
162
|
+
if (normalized) changed[id] = normalized;
|
|
163
|
+
}
|
|
164
|
+
if (Object.keys(changed).length) override.changed = changed;
|
|
165
|
+
else delete override.changed;
|
|
166
|
+
}
|
|
167
|
+
return override;
|
|
80
168
|
}
|
|
81
169
|
/**
|
|
82
170
|
* Structural validation of an unknown value (parsed JSON). Returns the
|
|
@@ -84,56 +172,130 @@ function checkOverview(value, at, errors) {
|
|
|
84
172
|
* recipes sorted and unique) or every problem found, each a readable line
|
|
85
173
|
* naming the path. Unknown top-level keys are kept (additive fields in v1).
|
|
86
174
|
*
|
|
87
|
-
* Deep checks
|
|
88
|
-
*
|
|
89
|
-
*
|
|
175
|
+
* Deep checks use optional host-supplied catalogs. A structurally valid
|
|
176
|
+
* cookbook remains portable when those catalogs are absent. Invalid overview
|
|
177
|
+
* entries are dropped and reported, following the v1 overview contract.
|
|
90
178
|
*/
|
|
91
179
|
function validateCookbook(input, options = {}) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
180
|
+
const diagnostics = [];
|
|
181
|
+
if (!isObject(input)) {
|
|
182
|
+
error(diagnostics, "invalid_type", "", "expected a JSON object");
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
errors: ["expected a JSON object"],
|
|
186
|
+
diagnostics
|
|
187
|
+
};
|
|
188
|
+
}
|
|
98
189
|
const { $schema, version } = input;
|
|
99
|
-
if ($schema !== void 0 && $schema !== "https://s-m-r-t.dev/schemas/cookbook/v1.json" && $schema !== "https://s-m-r-t.dev/schemas/blueprint/v1.json")
|
|
100
|
-
if (version === void 0)
|
|
101
|
-
else if (typeof version !== "number" || !Number.isInteger(version))
|
|
102
|
-
else if (version > 1)
|
|
103
|
-
else if (version !== 1)
|
|
104
|
-
if ($schema === "https://s-m-r-t.dev/schemas/blueprint/v1.json")
|
|
105
|
-
for (const key of ["name", "description"]) if (input[key] !== void 0 && typeof input[key] !== "string")
|
|
190
|
+
if ($schema !== void 0 && $schema !== "https://s-m-r-t.dev/schemas/cookbook/v1.json" && $schema !== "https://s-m-r-t.dev/schemas/blueprint/v1.json") error(diagnostics, "invalid_schema", "$schema", `$schema must be ${COOKBOOK_SCHEMA_URL}`);
|
|
191
|
+
if (version === void 0) error(diagnostics, "missing_field", "version", "version is required");
|
|
192
|
+
else if (typeof version !== "number" || !Number.isInteger(version)) error(diagnostics, "invalid_type", "version", "version must be a whole number");
|
|
193
|
+
else if (version > 1) error(diagnostics, "unsupported_version", "version", `version ${version} is newer than this reader understands (1)`);
|
|
194
|
+
else if (version !== 1) error(diagnostics, "unsupported_version", "version", `version ${version} is not supported`);
|
|
195
|
+
if ($schema === "https://s-m-r-t.dev/schemas/blueprint/v1.json") warning(diagnostics, "migrated_schema", "$schema", "$schema uses the legacy blueprint URL; migrated");
|
|
196
|
+
for (const key of ["name", "description"]) if (input[key] !== void 0 && typeof input[key] !== "string") error(diagnostics, "invalid_type", key, `${key} must be a string`);
|
|
106
197
|
let recipes = [];
|
|
107
|
-
if (!isStrings(input.recipes))
|
|
198
|
+
if (!isStrings(input.recipes)) error(diagnostics, "invalid_type", "recipes", "recipes must be a list of recipe ids");
|
|
108
199
|
else {
|
|
109
200
|
recipes = [...new Set(input.recipes)].sort();
|
|
110
201
|
if (options.recipes) {
|
|
111
202
|
const known = new Set(options.recipes);
|
|
112
|
-
for (const id of recipes) if (!known.has(id))
|
|
203
|
+
for (const id of recipes) if (!known.has(id)) error(diagnostics, "unknown_recipe", `recipes[${input.recipes.indexOf(id)}]`, `recipes: unknown recipe "${id}"`);
|
|
113
204
|
}
|
|
114
205
|
}
|
|
115
206
|
let features = [];
|
|
116
|
-
if (input.features !== void 0) if (!isStrings(input.features))
|
|
207
|
+
if (input.features !== void 0) if (!isStrings(input.features)) error(diagnostics, "invalid_type", "features", "features must be a list of model names");
|
|
117
208
|
else {
|
|
118
209
|
features = [...new Set(input.features)].sort();
|
|
119
|
-
if (features.length !== input.features.length)
|
|
210
|
+
if (features.length !== input.features.length) warning(diagnostics, "duplicate_value", "features", "features listed a model more than once; deduplicated");
|
|
120
211
|
}
|
|
121
212
|
const policies = [];
|
|
122
|
-
if (!Array.isArray(input.policies))
|
|
213
|
+
if (!Array.isArray(input.policies)) error(diagnostics, "invalid_type", "policies", "policies must be a list");
|
|
123
214
|
else for (const [i, row] of input.policies.entries()) {
|
|
124
|
-
checkPolicy(row, `policies[${i}]`,
|
|
215
|
+
checkPolicy(row, `policies[${i}]`, diagnostics);
|
|
125
216
|
policies.push(row);
|
|
126
217
|
}
|
|
127
218
|
if (input.exposure !== void 0) {
|
|
128
|
-
if (!isStringRecord(input.exposure, (s) => isStrings(s) && s.every((x) => SURFACES.has(x))))
|
|
219
|
+
if (!isStringRecord(input.exposure, (s) => isStrings(s) && s.every((x) => SURFACES.has(x)))) error(diagnostics, "invalid_value", "exposure", "exposure must map model names to lists of api, mcp or cli");
|
|
220
|
+
}
|
|
221
|
+
if (input.layout !== void 0) checkLayout(input.layout, diagnostics);
|
|
222
|
+
if (input.theme !== void 0) checkTheme(input.theme, diagnostics);
|
|
223
|
+
if (isObject(input.theme) && typeof input.theme.preset === "string") {
|
|
224
|
+
const presets = options.themePresets ? new Set(options.themePresets) : void 0;
|
|
225
|
+
if (presets && !presets.has(input.theme.preset)) error(diagnostics, "unknown_theme_preset", "theme.preset", `Unknown theme preset "${input.theme.preset}".`);
|
|
226
|
+
}
|
|
227
|
+
if (isObject(input.layout) && input.layout.version === 1 && options.layout) {
|
|
228
|
+
const sections = options.layout.sections ? new Set(options.layout.sections) : void 0;
|
|
229
|
+
const items = options.layout.items ? new Set(options.layout.items) : void 0;
|
|
230
|
+
const placementItems = options.layout.placementItems ? new Set(options.layout.placementItems) : void 0;
|
|
231
|
+
if (sections && Array.isArray(input.layout.customSections)) {
|
|
232
|
+
for (const section of input.layout.customSections) if (isObject(section) && typeof section.id === "string") sections.add(section.id);
|
|
233
|
+
}
|
|
234
|
+
const known = (id, kind) => kind === "section" ? sections?.has(id) !== false : kind === "item" ? items?.has(id) !== false : sections?.has(id) === true || items?.has(id) === true || !sections && !items;
|
|
235
|
+
const check = (id, path, kind) => {
|
|
236
|
+
if (typeof id === "string" && !known(id, kind)) error(diagnostics, "unknown_layout_id", path, `Unknown layout ${kind === "either" ? "section or item" : kind} "${id}".`);
|
|
237
|
+
};
|
|
238
|
+
if (isStrings(input.layout.sectionOrder)) input.layout.sectionOrder.forEach((id, index) => {
|
|
239
|
+
check(id, `layout.sectionOrder[${index}]`, "section");
|
|
240
|
+
});
|
|
241
|
+
if (isStrings(input.layout.hidden)) input.layout.hidden.forEach((id, index) => {
|
|
242
|
+
check(id, `layout.hidden[${index}]`, "either");
|
|
243
|
+
});
|
|
244
|
+
if (isObject(input.layout.itemOrder)) for (const [section, order] of Object.entries(input.layout.itemOrder)) {
|
|
245
|
+
check(section, `layout.itemOrder.${section}`, "section");
|
|
246
|
+
if (isStrings(order)) order.forEach((id, index) => {
|
|
247
|
+
check(id, `layout.itemOrder.${section}[${index}]`, "item");
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (isObject(input.layout.moved)) for (const [id, section] of Object.entries(input.layout.moved)) {
|
|
251
|
+
check(id, `layout.moved.${id}`, "item");
|
|
252
|
+
check(section, `layout.moved.${id}`, "section");
|
|
253
|
+
}
|
|
254
|
+
for (const key of ["sections", "items"]) {
|
|
255
|
+
if (!isObject(input.layout[key])) continue;
|
|
256
|
+
for (const id of Object.keys(input.layout[key])) check(id, `layout.${key}.${id}`, key === "sections" ? "section" : "item");
|
|
257
|
+
}
|
|
258
|
+
if (isObject(input.layout.placements)) {
|
|
259
|
+
for (const id of Object.keys(input.layout.placements)) if (placementItems && !placementItems.has(id)) error(diagnostics, "unknown_layout_id", `layout.placements.${id}`, `Unknown layout placement item "${id}".`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const overviews = {};
|
|
263
|
+
if (input.overviews !== void 0) if (!isObject(input.overviews) || Object.hasOwn(input.overviews, "__proto__")) error(diagnostics, "invalid_type", "overviews", "overviews must map overview ids to overrides");
|
|
264
|
+
else {
|
|
265
|
+
const overviewIds = options.overviewIds ? new Set(options.overviewIds) : void 0;
|
|
266
|
+
for (const [id, value] of Object.entries(input.overviews)) {
|
|
267
|
+
const at = `overviews.${id}`;
|
|
268
|
+
if (!OVERVIEW_ID.test(id)) {
|
|
269
|
+
warning(diagnostics, "invalid_overview_id", at, `${at} was dropped because its overview id is invalid`);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (overviewIds && !overviewIds.has(id)) {
|
|
273
|
+
warning(diagnostics, "unknown_overview", at, `Unknown overview "${id}" was dropped.`);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const override = normalizeOverview(value, at, diagnostics);
|
|
277
|
+
if (!override) continue;
|
|
278
|
+
if (!options.validateOverview) {
|
|
279
|
+
overviews[id] = override;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
let checked;
|
|
283
|
+
try {
|
|
284
|
+
checked = options.validateOverview(id, override);
|
|
285
|
+
} catch {
|
|
286
|
+
warning(diagnostics, "overview_validation_failed", at, `Overview "${id}" was dropped because its host validator failed.`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
for (const issue of checked.diagnostics ?? []) warning(diagnostics, issue.code, issue.path || at, issue.message);
|
|
290
|
+
if (!checked.ok && checked.diagnostics.length === 0) warning(diagnostics, "overview_rejected", at, `Overview "${id}" was rejected by its host validator and dropped.`);
|
|
291
|
+
if (checked.ok && checked.override) overviews[id] = checked.override;
|
|
292
|
+
}
|
|
129
293
|
}
|
|
130
|
-
|
|
131
|
-
if (input.theme !== void 0) checkTheme(input.theme, errors);
|
|
132
|
-
if (input.overviews !== void 0) if (!isObject(input.overviews) || Object.hasOwn(input.overviews, "__proto__")) errors.push("overviews must map overview ids to overrides");
|
|
133
|
-
else for (const [id, value] of Object.entries(input.overviews)) checkOverview(value, `overviews.${id}`, errors);
|
|
294
|
+
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
|
|
134
295
|
if (errors.length) return {
|
|
135
296
|
ok: false,
|
|
136
|
-
errors
|
|
297
|
+
errors,
|
|
298
|
+
diagnostics
|
|
137
299
|
};
|
|
138
300
|
const cookbook = {
|
|
139
301
|
...input,
|
|
@@ -146,11 +308,13 @@ function validateCookbook(input, options = {}) {
|
|
|
146
308
|
if (input.exposure !== void 0) cookbook.exposure = input.exposure;
|
|
147
309
|
if (input.layout !== void 0) cookbook.layout = input.layout;
|
|
148
310
|
if (input.theme !== void 0) cookbook.theme = input.theme;
|
|
149
|
-
|
|
311
|
+
delete cookbook.overviews;
|
|
312
|
+
if (Object.keys(overviews).length) cookbook.overviews = overviews;
|
|
150
313
|
return {
|
|
151
314
|
ok: true,
|
|
152
315
|
cookbook,
|
|
153
|
-
warnings
|
|
316
|
+
warnings: diagnostics.filter((diagnostic) => diagnostic.severity === "warning").map((diagnostic) => diagnostic.message),
|
|
317
|
+
diagnostics
|
|
154
318
|
};
|
|
155
319
|
}
|
|
156
320
|
/** Parse JSON text, then {@link validateCookbook} it. */
|
|
@@ -161,7 +325,13 @@ function parseCookbookText(text, options = {}) {
|
|
|
161
325
|
} catch {
|
|
162
326
|
return {
|
|
163
327
|
ok: false,
|
|
164
|
-
errors: ["not valid JSON"]
|
|
328
|
+
errors: ["not valid JSON"],
|
|
329
|
+
diagnostics: [{
|
|
330
|
+
severity: "error",
|
|
331
|
+
code: "invalid_json",
|
|
332
|
+
path: "",
|
|
333
|
+
message: "not valid JSON"
|
|
334
|
+
}]
|
|
165
335
|
};
|
|
166
336
|
}
|
|
167
337
|
return validateCookbook(value, options);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.js","names":[],"sources":["../../src/cookbook/validate.ts"],"sourcesContent":["import type {\n Cookbook,\n CookbookExposureSurface,\n CookbookLayout,\n CookbookOverviewOverride,\n CookbookPolicyRow,\n CookbookTheme,\n} from '@happyvertical/smrt-types';\n\nexport type { Cookbook } from '@happyvertical/smrt-types';\n\n/** What a v1 document carries in `$schema`. */\nexport const COOKBOOK_SCHEMA_URL =\n 'https://s-m-r-t.dev/schemas/cookbook/v1.json';\n/** Accepted on read (the document used to be called a blueprint); never written. */\nexport const COOKBOOK_LEGACY_SCHEMA_URL =\n 'https://s-m-r-t.dev/schemas/blueprint/v1.json';\nexport const COOKBOOK_VERSION = 1;\n\nexport interface CookbookValidateOptions {\n /**\n * The recipe ids that exist (from published manifests). When given, an id\n * outside it is an error. When absent no existence check runs: that is the\n * seam `smrt cookbook validate` fills.\n */\n recipes?: Iterable<string>;\n}\n\nexport type CookbookValidation =\n | { ok: true; cookbook: Cookbook; warnings: string[] }\n | { ok: false; errors: string[] };\n\nconst VISIBILITIES = new Set(['basic', 'advanced', 'hidden']);\nconst SURFACES = new Set(['api', 'mcp', 'cli']);\nconst SCHEMES = new Set(['light', 'dark', 'system']);\n\nconst isObject = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v);\nconst isStrings = (v: unknown): v is string[] =>\n Array.isArray(v) && v.every((x) => typeof x === 'string');\nconst isStringRecord = (v: unknown, valid: (x: unknown) => boolean) =>\n isObject(v) &&\n !Object.hasOwn(v, '__proto__') &&\n Object.values(v).every(valid);\n\nfunction checkPolicy(value: unknown, at: string, errors: string[]) {\n if (!isObject(value)) return errors.push(`${at} must be an object`);\n for (const key of ['objectRef', 'fieldName'] as const) {\n if (typeof value[key] !== 'string' || !value[key]) {\n errors.push(`${at}.${key} must be a non-empty string`);\n }\n }\n if (value.scopeType !== 'app') errors.push(`${at}.scopeType must be \"app\"`);\n for (const key of ['defaultValue', 'help', 'label'] as const) {\n const v = value[key];\n if (v !== undefined && v !== null && typeof v !== 'string') {\n errors.push(`${at}.${key} must be a string or null`);\n }\n }\n const vis = value.visibility;\n if (vis != null && !(typeof vis === 'string' && VISIBILITIES.has(vis))) {\n errors.push(`${at}.visibility must be basic, advanced or hidden`);\n }\n const order = value.displayOrder;\n if (order != null && !(typeof order === 'number' && Number.isFinite(order))) {\n errors.push(`${at}.displayOrder must be a finite number`);\n }\n if (value.locked != null && typeof value.locked !== 'boolean') {\n errors.push(`${at}.locked must be a boolean`);\n }\n}\n\nfunction checkLayout(value: unknown, errors: string[]) {\n if (!isObject(value) || value.version !== 1) {\n return errors.push('layout must be an object with version 1');\n }\n for (const key of ['sectionOrder', 'hidden'] as const) {\n if (value[key] !== undefined && !isStrings(value[key])) {\n errors.push(`layout.${key} must be a list of strings`);\n }\n }\n for (const key of ['itemOrder', 'moved', 'placements'] as const) {\n const v = value[key];\n if (v === undefined) continue;\n const ok =\n key === 'itemOrder'\n ? isStringRecord(v, isStrings)\n : isStringRecord(v, (x) => typeof x === 'string');\n if (!ok) {\n errors.push(\n key === 'itemOrder'\n ? 'layout.itemOrder must map ids to lists of strings'\n : `layout.${key} must map ids to strings`,\n );\n }\n }\n for (const key of ['sections', 'items', 'panels'] as const) {\n if (\n value[key] !== undefined &&\n !isStringRecord(value[key], (x) => isObject(x))\n ) {\n errors.push(`layout.${key} must map ids to objects`);\n }\n }\n const custom = value.customSections;\n if (\n custom !== undefined &&\n !(\n Array.isArray(custom) &&\n custom.every(\n (c) =>\n isObject(c) &&\n typeof c.id === 'string' &&\n typeof c.label === 'string',\n )\n )\n ) {\n errors.push('layout.customSections must be a list of { id, label }');\n }\n}\n\nfunction checkTheme(value: unknown, errors: string[]) {\n if (!isObject(value)) return errors.push('theme must be an object');\n if (value.preset !== undefined && typeof value.preset !== 'string') {\n errors.push('theme.preset must be a string');\n }\n if (\n value.colorScheme !== undefined &&\n !(typeof value.colorScheme === 'string' && SCHEMES.has(value.colorScheme))\n ) {\n errors.push('theme.colorScheme must be light, dark or system');\n }\n const custom = value.custom;\n if (custom !== undefined) {\n if (!isObject(custom) || typeof custom.primary !== 'string') {\n errors.push('theme.custom must be an object with a string primary');\n } else if (!/^#[0-9a-fA-F]{6}$/.test(custom.primary)) {\n errors.push('theme.custom.primary must be a hex colour like #c2410c');\n } else if (\n custom.fontFamily !== undefined &&\n typeof custom.fontFamily !== 'string'\n ) {\n errors.push('theme.custom.fontFamily must be a string');\n }\n }\n}\n\nfunction checkOverview(value: unknown, at: string, errors: string[]) {\n if (!isObject(value) || value.version !== 1) {\n return errors.push(`${at} must be an object with version 1`);\n }\n for (const key of ['order', 'removed'] as const) {\n if (value[key] !== undefined && !isStrings(value[key])) {\n errors.push(`${at}.${key} must be a list of strings`);\n }\n }\n const added = value.added;\n if (\n added !== undefined &&\n !(Array.isArray(added) && added.every((w) => isObject(w)))\n ) {\n errors.push(`${at}.added must be a list of widgets`);\n }\n if (\n value.changed !== undefined &&\n !isStringRecord(value.changed, (x) => isObject(x))\n ) {\n errors.push(`${at}.changed must map widget ids to objects`);\n }\n}\n\n/**\n * Structural validation of an unknown value (parsed JSON). Returns the\n * normalised document (legacy `$schema` rewritten, `features` defaulted,\n * recipes sorted and unique) or every problem found, each a readable line\n * naming the path. Unknown top-level keys are kept (additive fields in v1).\n *\n * Deep checks that need other packages (overview widgets against a page\n * definition, nav ids against recipes, theme presets) are the consumer's job;\n * recipe existence runs only when `options.recipes` is given.\n */\nexport function validateCookbook(\n input: unknown,\n options: CookbookValidateOptions = {},\n): CookbookValidation {\n if (!isObject(input)) {\n return { ok: false, errors: ['expected a JSON object'] };\n }\n const errors: string[] = [];\n const warnings: string[] = [];\n\n const { $schema, version } = input;\n if (\n $schema !== undefined &&\n $schema !== COOKBOOK_SCHEMA_URL &&\n $schema !== COOKBOOK_LEGACY_SCHEMA_URL\n ) {\n errors.push(`$schema must be ${COOKBOOK_SCHEMA_URL}`);\n }\n if (version === undefined) {\n errors.push('version is required');\n } else if (typeof version !== 'number' || !Number.isInteger(version)) {\n errors.push('version must be a whole number');\n } else if (version > COOKBOOK_VERSION) {\n errors.push(\n `version ${version} is newer than this reader understands (${COOKBOOK_VERSION})`,\n );\n } else if (version !== COOKBOOK_VERSION) {\n errors.push(`version ${version} is not supported`);\n }\n if ($schema === COOKBOOK_LEGACY_SCHEMA_URL) {\n warnings.push('$schema uses the legacy blueprint URL; migrated');\n }\n\n for (const key of ['name', 'description'] as const) {\n if (input[key] !== undefined && typeof input[key] !== 'string') {\n errors.push(`${key} must be a string`);\n }\n }\n\n let recipes: string[] = [];\n if (!isStrings(input.recipes)) {\n errors.push('recipes must be a list of recipe ids');\n } else {\n recipes = [...new Set(input.recipes)].sort();\n if (options.recipes) {\n const known = new Set(options.recipes);\n for (const id of recipes) {\n if (!known.has(id)) errors.push(`recipes: unknown recipe \"${id}\"`);\n }\n }\n }\n\n let features: string[] = [];\n if (input.features !== undefined) {\n if (!isStrings(input.features)) {\n errors.push('features must be a list of model names');\n } else {\n features = [...new Set(input.features)].sort();\n if (features.length !== input.features.length) {\n warnings.push('features listed a model more than once; deduplicated');\n }\n }\n }\n\n const policies: CookbookPolicyRow[] = [];\n if (!Array.isArray(input.policies)) {\n errors.push('policies must be a list');\n } else {\n for (const [i, row] of input.policies.entries()) {\n checkPolicy(row, `policies[${i}]`, errors);\n policies.push(row as CookbookPolicyRow);\n }\n }\n\n if (input.exposure !== undefined) {\n const ok = isStringRecord(\n input.exposure,\n (s) => isStrings(s) && s.every((x) => SURFACES.has(x)),\n );\n if (!ok)\n errors.push('exposure must map model names to lists of api, mcp or cli');\n }\n if (input.layout !== undefined) checkLayout(input.layout, errors);\n if (input.theme !== undefined) checkTheme(input.theme, errors);\n if (input.overviews !== undefined) {\n if (\n !isObject(input.overviews) ||\n Object.hasOwn(input.overviews, '__proto__')\n ) {\n errors.push('overviews must map overview ids to overrides');\n } else {\n for (const [id, value] of Object.entries(input.overviews)) {\n checkOverview(value, `overviews.${id}`, errors);\n }\n }\n }\n\n if (errors.length) return { ok: false, errors };\n\n const cookbook = {\n ...input,\n $schema: COOKBOOK_SCHEMA_URL,\n version: COOKBOOK_VERSION,\n recipes,\n features,\n policies,\n } as unknown as Cookbook;\n if (input.exposure !== undefined) {\n cookbook.exposure = input.exposure as Record<\n string,\n CookbookExposureSurface[]\n >;\n }\n if (input.layout !== undefined)\n cookbook.layout = input.layout as CookbookLayout;\n if (input.theme !== undefined) cookbook.theme = input.theme as CookbookTheme;\n if (input.overviews !== undefined) {\n cookbook.overviews = input.overviews as Record<\n string,\n CookbookOverviewOverride\n >;\n }\n return { ok: true, cookbook, warnings };\n}\n\n/** Parse JSON text, then {@link validateCookbook} it. */\nexport function parseCookbookText(\n text: string,\n options: CookbookValidateOptions = {},\n): CookbookValidation {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return { ok: false, errors: ['not valid JSON'] };\n }\n return validateCookbook(value, options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validate.js","names":[],"sources":["../../src/cookbook/validate.ts"],"sourcesContent":["import type {\n Cookbook,\n CookbookExposureSurface,\n CookbookLayout,\n CookbookOverviewOverride,\n CookbookOverviewWidget,\n CookbookPolicyRow,\n CookbookTheme,\n RecipeShellSlot,\n} from '@happyvertical/smrt-types';\n\nexport type { Cookbook } from '@happyvertical/smrt-types';\n\n/** What a v1 document carries in `$schema`. */\nexport const COOKBOOK_SCHEMA_URL =\n 'https://s-m-r-t.dev/schemas/cookbook/v1.json';\n/** Accepted on read (the document used to be called a blueprint); never written. */\nexport const COOKBOOK_LEGACY_SCHEMA_URL =\n 'https://s-m-r-t.dev/schemas/blueprint/v1.json';\nexport const COOKBOOK_VERSION = 1;\n\nexport interface CookbookValidateOptions {\n /**\n * The recipe ids that exist (from published manifests). When given, an id\n * outside it is an error. When absent no existence check runs: that is the\n * seam `smrt cookbook validate` fills.\n */\n recipes?: Iterable<string>;\n /** Built-in or host-registered theme preset ids. */\n themePresets?: Iterable<string>;\n /** Manifest-derived ids the cookbook layout may reference. */\n layout?: {\n sections?: Iterable<string>;\n items?: Iterable<string>;\n /** Movable shell item ids resolved by the host (`dock:`, `slot:`, or host ids). */\n placementItems?: Iterable<string>;\n };\n /** Overview page ids defined by the resolved app shell. */\n overviewIds?: Iterable<string>;\n /**\n * Validate and canonicalize one structurally valid override against its page\n * definition and widget registry. The callback is trusted host code; the\n * cookbook value remains plain data and is never executed.\n */\n validateOverview?: (\n id: string,\n override: CookbookOverviewOverride,\n ) => CookbookOverviewValidation;\n}\n\nexport interface CookbookDiagnostic {\n severity: 'error' | 'warning';\n /** Stable machine-readable category. Host validators may add their own. */\n code: string;\n /** Path into the cookbook, using the existing dotted/indexed notation. */\n path: string;\n message: string;\n}\n\nexport type CookbookOverviewValidation =\n | {\n ok: true;\n /** `null` means the canonical override is empty and should be omitted. */\n override: CookbookOverviewOverride | null;\n diagnostics?: readonly CookbookDiagnostic[];\n }\n | { ok: false; diagnostics: readonly CookbookDiagnostic[] };\n\nexport type CookbookValidation =\n | {\n ok: true;\n cookbook: Cookbook;\n warnings: string[];\n diagnostics: CookbookDiagnostic[];\n }\n | { ok: false; errors: string[]; diagnostics: CookbookDiagnostic[] };\n\nconst VISIBILITIES = new Set(['basic', 'advanced', 'hidden']);\nconst SURFACES = new Set(['api', 'mcp', 'cli']);\nconst SCHEMES = new Set(['light', 'dark', 'system']);\nconst SHELL_SLOT_VALUES = [\n 'header.start',\n 'header.center',\n 'header.end',\n 'footer.start',\n 'footer.center',\n 'footer.end',\n 'leftSidebar.header',\n 'leftSidebar.footer',\n 'rightSidebar.header',\n 'rightSidebar.footer',\n] as const satisfies readonly RecipeShellSlot[];\nconst SHELL_SLOTS = new Set<string>(SHELL_SLOT_VALUES);\n\nconst isObject = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v);\nconst isStrings = (v: unknown): v is string[] =>\n Array.isArray(v) && v.every((x) => typeof x === 'string');\nconst isStringRecord = (v: unknown, valid: (x: unknown) => boolean) =>\n isObject(v) &&\n !Object.hasOwn(v, '__proto__') &&\n Object.values(v).every(valid);\n\nconst add = (\n diagnostics: CookbookDiagnostic[],\n severity: CookbookDiagnostic['severity'],\n code: string,\n path: string,\n message: string,\n): void => {\n diagnostics.push({ severity, code, path, message });\n};\n\nconst error = (\n diagnostics: CookbookDiagnostic[],\n code: string,\n path: string,\n message: string,\n): void => add(diagnostics, 'error', code, path, message);\n\nconst warning = (\n diagnostics: CookbookDiagnostic[],\n code: string,\n path: string,\n message: string,\n): void => add(diagnostics, 'warning', code, path, message);\n\nfunction checkPolicy(\n value: unknown,\n at: string,\n diagnostics: CookbookDiagnostic[],\n) {\n if (!isObject(value))\n return error(diagnostics, 'invalid_type', at, `${at} must be an object`);\n for (const key of ['objectRef', 'fieldName'] as const) {\n if (typeof value[key] !== 'string' || !value[key]) {\n error(\n diagnostics,\n 'invalid_value',\n `${at}.${key}`,\n `${at}.${key} must be a non-empty string`,\n );\n }\n }\n if (value.scopeType !== 'app')\n error(\n diagnostics,\n 'invalid_value',\n `${at}.scopeType`,\n `${at}.scopeType must be \"app\"`,\n );\n for (const key of ['defaultValue', 'help', 'label'] as const) {\n const v = value[key];\n if (v !== undefined && v !== null && typeof v !== 'string') {\n error(\n diagnostics,\n 'invalid_type',\n `${at}.${key}`,\n `${at}.${key} must be a string or null`,\n );\n }\n }\n const vis = value.visibility;\n if (vis != null && !(typeof vis === 'string' && VISIBILITIES.has(vis))) {\n error(\n diagnostics,\n 'invalid_value',\n `${at}.visibility`,\n `${at}.visibility must be basic, advanced or hidden`,\n );\n }\n const order = value.displayOrder;\n if (order != null && !(typeof order === 'number' && Number.isFinite(order))) {\n error(\n diagnostics,\n 'invalid_value',\n `${at}.displayOrder`,\n `${at}.displayOrder must be a finite number`,\n );\n }\n if (value.locked != null && typeof value.locked !== 'boolean') {\n error(\n diagnostics,\n 'invalid_type',\n `${at}.locked`,\n `${at}.locked must be a boolean`,\n );\n }\n}\n\nfunction checkLayout(value: unknown, diagnostics: CookbookDiagnostic[]) {\n if (!isObject(value) || value.version !== 1) {\n return error(\n diagnostics,\n 'invalid_value',\n 'layout',\n 'layout must be an object with version 1',\n );\n }\n for (const key of ['sectionOrder', 'hidden'] as const) {\n if (value[key] !== undefined && !isStrings(value[key])) {\n error(\n diagnostics,\n 'invalid_type',\n `layout.${key}`,\n `layout.${key} must be a list of strings`,\n );\n }\n }\n for (const key of ['itemOrder', 'moved', 'placements'] as const) {\n const v = value[key];\n if (v === undefined) continue;\n const ok =\n key === 'itemOrder'\n ? isStringRecord(v, isStrings)\n : isStringRecord(v, (x) => typeof x === 'string');\n if (!ok) {\n error(\n diagnostics,\n 'invalid_type',\n `layout.${key}`,\n key === 'itemOrder'\n ? 'layout.itemOrder must map ids to lists of strings'\n : `layout.${key} must map ids to strings`,\n );\n }\n }\n if (isObject(value.placements)) {\n for (const [id, slot] of Object.entries(value.placements)) {\n if (typeof slot === 'string' && !SHELL_SLOTS.has(slot)) {\n error(\n diagnostics,\n 'unknown_layout_slot',\n `layout.placements.${id}`,\n `Unknown shell slot \"${slot}\".`,\n );\n }\n }\n }\n for (const key of ['sections', 'items', 'panels'] as const) {\n if (\n value[key] !== undefined &&\n !isStringRecord(value[key], (x) => isObject(x))\n ) {\n error(\n diagnostics,\n 'invalid_type',\n `layout.${key}`,\n `layout.${key} must map ids to objects`,\n );\n }\n }\n const custom = value.customSections;\n if (\n custom !== undefined &&\n !(\n Array.isArray(custom) &&\n custom.every(\n (c) =>\n isObject(c) &&\n typeof c.id === 'string' &&\n typeof c.label === 'string',\n )\n )\n ) {\n error(\n diagnostics,\n 'invalid_type',\n 'layout.customSections',\n 'layout.customSections must be a list of { id, label }',\n );\n }\n}\n\nfunction checkTheme(value: unknown, diagnostics: CookbookDiagnostic[]) {\n if (!isObject(value))\n return error(\n diagnostics,\n 'invalid_type',\n 'theme',\n 'theme must be an object',\n );\n if (value.preset !== undefined && typeof value.preset !== 'string') {\n error(\n diagnostics,\n 'invalid_type',\n 'theme.preset',\n 'theme.preset must be a string',\n );\n }\n if (\n value.colorScheme !== undefined &&\n !(typeof value.colorScheme === 'string' && SCHEMES.has(value.colorScheme))\n ) {\n error(\n diagnostics,\n 'invalid_value',\n 'theme.colorScheme',\n 'theme.colorScheme must be light, dark or system',\n );\n }\n const custom = value.custom;\n if (custom !== undefined) {\n if (!isObject(custom) || typeof custom.primary !== 'string') {\n error(\n diagnostics,\n 'invalid_value',\n 'theme.custom',\n 'theme.custom must be an object with a string primary',\n );\n } else if (!/^#[0-9a-fA-F]{6}$/.test(custom.primary)) {\n error(\n diagnostics,\n 'invalid_value',\n 'theme.custom.primary',\n 'theme.custom.primary must be a hex colour like #c2410c',\n );\n } else if (\n custom.fontFamily !== undefined &&\n typeof custom.fontFamily !== 'string'\n ) {\n error(\n diagnostics,\n 'invalid_type',\n 'theme.custom.fontFamily',\n 'theme.custom.fontFamily must be a string',\n );\n }\n }\n}\n\nconst OVERVIEW_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/;\nconst WIDGET_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;\nconst WIDGET_TYPE = /^[a-z][a-z0-9-]{0,31}$/;\n\nconst isOptionValue = (value: unknown) =>\n value === null ||\n typeof value === 'string' ||\n (typeof value === 'number' && Number.isFinite(value)) ||\n typeof value === 'boolean';\n\nfunction normalizeOverviewWidget(\n value: unknown,\n at: string,\n diagnostics: CookbookDiagnostic[],\n): CookbookOverviewWidget | null {\n if (!isObject(value)) {\n warning(diagnostics, 'malformed_overview_widget', at, `${at} was dropped`);\n return null;\n }\n const valid = [\n typeof value.id === 'string' && WIDGET_ID.test(value.id),\n typeof value.type === 'string' && WIDGET_TYPE.test(value.type),\n typeof value.span === 'number' &&\n Number.isInteger(value.span) &&\n value.span >= 1 &&\n value.span <= 4,\n isObject(value.options) &&\n Object.values(value.options).every(isOptionValue),\n value.version === undefined ||\n (typeof value.version === 'number' &&\n Number.isInteger(value.version) &&\n value.version >= 1),\n ];\n if (valid.every(Boolean)) return value as unknown as CookbookOverviewWidget;\n warning(\n diagnostics,\n 'malformed_overview_widget',\n at,\n `${at} was dropped because it is not a valid overview widget`,\n );\n return null;\n}\n\nfunction normalizeOverviewPatch(\n value: unknown,\n at: string,\n diagnostics: CookbookDiagnostic[],\n): NonNullable<CookbookOverviewOverride['changed']>[string] | null {\n if (!isObject(value)) {\n warning(diagnostics, 'malformed_overview_change', at, `${at} was dropped`);\n return null;\n }\n const valid =\n (value.span === undefined ||\n (typeof value.span === 'number' &&\n Number.isInteger(value.span) &&\n value.span >= 1 &&\n value.span <= 4)) &&\n (value.options === undefined ||\n (isObject(value.options) &&\n Object.values(value.options).every(isOptionValue))) &&\n (value.version === undefined ||\n (typeof value.version === 'number' &&\n Number.isInteger(value.version) &&\n value.version >= 1));\n if (valid) {\n return value as NonNullable<CookbookOverviewOverride['changed']>[string];\n }\n warning(\n diagnostics,\n 'malformed_overview_change',\n at,\n `${at} was dropped because it is not a valid widget change`,\n );\n return null;\n}\n\nfunction normalizeOverview(\n value: unknown,\n at: string,\n diagnostics: CookbookDiagnostic[],\n): CookbookOverviewOverride | null {\n if (!isObject(value) || value.version !== 1) {\n warning(\n diagnostics,\n 'malformed_overview',\n at,\n `${at} was dropped because it must be an object with version 1`,\n );\n return null;\n }\n const override = {\n ...value,\n version: 1,\n } as unknown as CookbookOverviewOverride;\n for (const key of ['order', 'removed'] as const) {\n if (value[key] !== undefined && !isStrings(value[key])) {\n warning(\n diagnostics,\n 'malformed_overview_list',\n `${at}.${key}`,\n `${at}.${key} was dropped because it must be a list of strings`,\n );\n delete override[key];\n } else if (value[key] !== undefined) {\n override[key] = [...new Set(value[key])];\n }\n }\n if (value.added !== undefined) {\n if (!Array.isArray(value.added)) {\n warning(\n diagnostics,\n 'malformed_overview_widgets',\n `${at}.added`,\n `${at}.added was dropped because it must be a list of widgets`,\n );\n delete override.added;\n } else {\n const added = value.added.flatMap((widget, index) => {\n const normalized = normalizeOverviewWidget(\n widget,\n `${at}.added[${index}]`,\n diagnostics,\n );\n return normalized ? [normalized] : [];\n });\n if (added.length) override.added = added;\n else delete override.added;\n }\n }\n if (value.changed !== undefined) {\n if (!isObject(value.changed) || Object.hasOwn(value.changed, '__proto__')) {\n warning(\n diagnostics,\n 'malformed_overview_changes',\n `${at}.changed`,\n `${at}.changed was dropped because it must map widget ids to objects`,\n );\n delete override.changed;\n } else {\n const changed: NonNullable<CookbookOverviewOverride['changed']> = {};\n for (const [id, patch] of Object.entries(value.changed)) {\n if (!WIDGET_ID.test(id)) {\n warning(\n diagnostics,\n 'malformed_overview_change',\n `${at}.changed.${id}`,\n `${at}.changed.${id} was dropped because its widget id is invalid`,\n );\n continue;\n }\n const normalized = normalizeOverviewPatch(\n patch,\n `${at}.changed.${id}`,\n diagnostics,\n );\n if (normalized) changed[id] = normalized;\n }\n if (Object.keys(changed).length) override.changed = changed;\n else delete override.changed;\n }\n }\n return override;\n}\n\n/**\n * Structural validation of an unknown value (parsed JSON). Returns the\n * normalised document (legacy `$schema` rewritten, `features` defaulted,\n * recipes sorted and unique) or every problem found, each a readable line\n * naming the path. Unknown top-level keys are kept (additive fields in v1).\n *\n * Deep checks use optional host-supplied catalogs. A structurally valid\n * cookbook remains portable when those catalogs are absent. Invalid overview\n * entries are dropped and reported, following the v1 overview contract.\n */\nexport function validateCookbook(\n input: unknown,\n options: CookbookValidateOptions = {},\n): CookbookValidation {\n const diagnostics: CookbookDiagnostic[] = [];\n if (!isObject(input)) {\n error(diagnostics, 'invalid_type', '', 'expected a JSON object');\n return { ok: false, errors: ['expected a JSON object'], diagnostics };\n }\n\n const { $schema, version } = input;\n if (\n $schema !== undefined &&\n $schema !== COOKBOOK_SCHEMA_URL &&\n $schema !== COOKBOOK_LEGACY_SCHEMA_URL\n ) {\n error(\n diagnostics,\n 'invalid_schema',\n '$schema',\n `$schema must be ${COOKBOOK_SCHEMA_URL}`,\n );\n }\n if (version === undefined) {\n error(diagnostics, 'missing_field', 'version', 'version is required');\n } else if (typeof version !== 'number' || !Number.isInteger(version)) {\n error(\n diagnostics,\n 'invalid_type',\n 'version',\n 'version must be a whole number',\n );\n } else if (version > COOKBOOK_VERSION) {\n error(\n diagnostics,\n 'unsupported_version',\n 'version',\n `version ${version} is newer than this reader understands (${COOKBOOK_VERSION})`,\n );\n } else if (version !== COOKBOOK_VERSION) {\n error(\n diagnostics,\n 'unsupported_version',\n 'version',\n `version ${version} is not supported`,\n );\n }\n if ($schema === COOKBOOK_LEGACY_SCHEMA_URL) {\n warning(\n diagnostics,\n 'migrated_schema',\n '$schema',\n '$schema uses the legacy blueprint URL; migrated',\n );\n }\n\n for (const key of ['name', 'description'] as const) {\n if (input[key] !== undefined && typeof input[key] !== 'string') {\n error(diagnostics, 'invalid_type', key, `${key} must be a string`);\n }\n }\n\n let recipes: string[] = [];\n if (!isStrings(input.recipes)) {\n error(\n diagnostics,\n 'invalid_type',\n 'recipes',\n 'recipes must be a list of recipe ids',\n );\n } else {\n recipes = [...new Set(input.recipes)].sort();\n if (options.recipes) {\n const known = new Set(options.recipes);\n for (const id of recipes) {\n if (!known.has(id))\n error(\n diagnostics,\n 'unknown_recipe',\n `recipes[${input.recipes.indexOf(id)}]`,\n `recipes: unknown recipe \"${id}\"`,\n );\n }\n }\n }\n\n let features: string[] = [];\n if (input.features !== undefined) {\n if (!isStrings(input.features)) {\n error(\n diagnostics,\n 'invalid_type',\n 'features',\n 'features must be a list of model names',\n );\n } else {\n features = [...new Set(input.features)].sort();\n if (features.length !== input.features.length) {\n warning(\n diagnostics,\n 'duplicate_value',\n 'features',\n 'features listed a model more than once; deduplicated',\n );\n }\n }\n }\n\n const policies: CookbookPolicyRow[] = [];\n if (!Array.isArray(input.policies)) {\n error(diagnostics, 'invalid_type', 'policies', 'policies must be a list');\n } else {\n for (const [i, row] of input.policies.entries()) {\n checkPolicy(row, `policies[${i}]`, diagnostics);\n policies.push(row as CookbookPolicyRow);\n }\n }\n\n if (input.exposure !== undefined) {\n const ok = isStringRecord(\n input.exposure,\n (s) => isStrings(s) && s.every((x) => SURFACES.has(x)),\n );\n if (!ok)\n error(\n diagnostics,\n 'invalid_value',\n 'exposure',\n 'exposure must map model names to lists of api, mcp or cli',\n );\n }\n if (input.layout !== undefined) checkLayout(input.layout, diagnostics);\n if (input.theme !== undefined) checkTheme(input.theme, diagnostics);\n\n if (isObject(input.theme) && typeof input.theme.preset === 'string') {\n const presets = options.themePresets\n ? new Set(options.themePresets)\n : undefined;\n if (presets && !presets.has(input.theme.preset)) {\n error(\n diagnostics,\n 'unknown_theme_preset',\n 'theme.preset',\n `Unknown theme preset \"${input.theme.preset}\".`,\n );\n }\n }\n\n if (isObject(input.layout) && input.layout.version === 1 && options.layout) {\n const sections = options.layout.sections\n ? new Set(options.layout.sections)\n : undefined;\n const items = options.layout.items\n ? new Set(options.layout.items)\n : undefined;\n const placementItems = options.layout.placementItems\n ? new Set(options.layout.placementItems)\n : undefined;\n if (sections && Array.isArray(input.layout.customSections)) {\n for (const section of input.layout.customSections) {\n if (isObject(section) && typeof section.id === 'string') {\n sections.add(section.id);\n }\n }\n }\n const known = (id: string, kind: 'section' | 'item' | 'either') =>\n kind === 'section'\n ? sections?.has(id) !== false\n : kind === 'item'\n ? items?.has(id) !== false\n : sections?.has(id) === true ||\n items?.has(id) === true ||\n (!sections && !items);\n const check = (\n id: unknown,\n path: string,\n kind: 'section' | 'item' | 'either',\n ) => {\n if (typeof id === 'string' && !known(id, kind)) {\n error(\n diagnostics,\n 'unknown_layout_id',\n path,\n `Unknown layout ${kind === 'either' ? 'section or item' : kind} \"${id}\".`,\n );\n }\n };\n if (isStrings(input.layout.sectionOrder)) {\n input.layout.sectionOrder.forEach((id, index) => {\n check(id, `layout.sectionOrder[${index}]`, 'section');\n });\n }\n if (isStrings(input.layout.hidden)) {\n input.layout.hidden.forEach((id, index) => {\n check(id, `layout.hidden[${index}]`, 'either');\n });\n }\n if (isObject(input.layout.itemOrder)) {\n for (const [section, order] of Object.entries(input.layout.itemOrder)) {\n check(section, `layout.itemOrder.${section}`, 'section');\n if (isStrings(order)) {\n order.forEach((id, index) => {\n check(id, `layout.itemOrder.${section}[${index}]`, 'item');\n });\n }\n }\n }\n if (isObject(input.layout.moved)) {\n for (const [id, section] of Object.entries(input.layout.moved)) {\n check(id, `layout.moved.${id}`, 'item');\n check(section, `layout.moved.${id}`, 'section');\n }\n }\n for (const key of ['sections', 'items'] as const) {\n if (!isObject(input.layout[key])) continue;\n for (const id of Object.keys(input.layout[key])) {\n check(\n id,\n `layout.${key}.${id}`,\n key === 'sections' ? 'section' : 'item',\n );\n }\n }\n if (isObject(input.layout.placements)) {\n for (const id of Object.keys(input.layout.placements)) {\n if (placementItems && !placementItems.has(id)) {\n error(\n diagnostics,\n 'unknown_layout_id',\n `layout.placements.${id}`,\n `Unknown layout placement item \"${id}\".`,\n );\n }\n }\n }\n }\n\n const overviews: Record<string, CookbookOverviewOverride> = {};\n if (input.overviews !== undefined) {\n if (\n !isObject(input.overviews) ||\n Object.hasOwn(input.overviews, '__proto__')\n ) {\n error(\n diagnostics,\n 'invalid_type',\n 'overviews',\n 'overviews must map overview ids to overrides',\n );\n } else {\n const overviewIds = options.overviewIds\n ? new Set(options.overviewIds)\n : undefined;\n for (const [id, value] of Object.entries(input.overviews)) {\n const at = `overviews.${id}`;\n if (!OVERVIEW_ID.test(id)) {\n warning(\n diagnostics,\n 'invalid_overview_id',\n at,\n `${at} was dropped because its overview id is invalid`,\n );\n continue;\n }\n if (overviewIds && !overviewIds.has(id)) {\n warning(\n diagnostics,\n 'unknown_overview',\n at,\n `Unknown overview \"${id}\" was dropped.`,\n );\n continue;\n }\n const override = normalizeOverview(value, at, diagnostics);\n if (!override) continue;\n if (!options.validateOverview) {\n overviews[id] = override;\n continue;\n }\n let checked: CookbookOverviewValidation;\n try {\n checked = options.validateOverview(id, override);\n } catch {\n warning(\n diagnostics,\n 'overview_validation_failed',\n at,\n `Overview \"${id}\" was dropped because its host validator failed.`,\n );\n continue;\n }\n for (const issue of checked.diagnostics ?? []) {\n warning(diagnostics, issue.code, issue.path || at, issue.message);\n }\n if (!checked.ok && checked.diagnostics.length === 0) {\n warning(\n diagnostics,\n 'overview_rejected',\n at,\n `Overview \"${id}\" was rejected by its host validator and dropped.`,\n );\n }\n if (checked.ok && checked.override) overviews[id] = checked.override;\n }\n }\n }\n\n const errors = diagnostics\n .filter((diagnostic) => diagnostic.severity === 'error')\n .map((diagnostic) => diagnostic.message);\n if (errors.length) return { ok: false, errors, diagnostics };\n\n const cookbook = {\n ...input,\n $schema: COOKBOOK_SCHEMA_URL,\n version: COOKBOOK_VERSION,\n recipes,\n features,\n policies,\n } as unknown as Cookbook;\n if (input.exposure !== undefined) {\n cookbook.exposure = input.exposure as Record<\n string,\n CookbookExposureSurface[]\n >;\n }\n if (input.layout !== undefined)\n cookbook.layout = input.layout as CookbookLayout;\n if (input.theme !== undefined) cookbook.theme = input.theme as CookbookTheme;\n delete cookbook.overviews;\n if (Object.keys(overviews).length) cookbook.overviews = overviews;\n const warnings = diagnostics\n .filter((diagnostic) => diagnostic.severity === 'warning')\n .map((diagnostic) => diagnostic.message);\n return { ok: true, cookbook, warnings, diagnostics };\n}\n\n/** Parse JSON text, then {@link validateCookbook} it. */\nexport function parseCookbookText(\n text: string,\n options: CookbookValidateOptions = {},\n): CookbookValidation {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return {\n ok: false,\n errors: ['not valid JSON'],\n diagnostics: [\n {\n severity: 'error',\n code: 'invalid_json',\n path: '',\n message: 'not valid JSON',\n },\n ],\n };\n }\n return validateCookbook(value, options);\n}\n"],"mappings":"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"}
|
package/dist/dispatch/bus.d.ts
CHANGED
|
@@ -295,7 +295,11 @@ export declare class DispatchBus {
|
|
|
295
295
|
* tenant or widen visibility to all tenants. Scoping rules:
|
|
296
296
|
*
|
|
297
297
|
* - tenancy off → no tenant filter (pre-tenancy behavior).
|
|
298
|
-
* - active tenant T → that tenant's dispatches
|
|
298
|
+
* - active tenant T → that tenant's dispatches only. Global (NULL-tenant)
|
|
299
|
+
* dispatches are system-only on reads (#3301): a global row's payload (for
|
|
300
|
+
* example a delivery emitted outside tenant context) may carry another
|
|
301
|
+
* tenant's recipients/subjects. Processing (`process`) still lets a tenant
|
|
302
|
+
* worker claim global work.
|
|
299
303
|
* - tenancy on but no active tenant → global (NULL) dispatches only
|
|
300
304
|
* (fail-closed; never all tenants).
|
|
301
305
|
*
|
|
@@ -307,9 +311,10 @@ export declare class DispatchBus {
|
|
|
307
311
|
* Gets a dispatch by ID, enforcing the active tenant scope (S5 #1398).
|
|
308
312
|
*
|
|
309
313
|
* Applies the same server-derived tenant predicate as {@link list}: a
|
|
310
|
-
* subscriber in tenant A cannot fetch tenant B's dispatch
|
|
311
|
-
*
|
|
312
|
-
*
|
|
314
|
+
* subscriber in tenant A cannot fetch tenant B's dispatch, or a global
|
|
315
|
+
* (NULL-tenant) dispatch, by id (#3301), and when tenancy is on with no
|
|
316
|
+
* active tenant only global (NULL) dispatches are returned. Returns `null`
|
|
317
|
+
* when the dispatch exists but is out of scope.
|
|
313
318
|
*/
|
|
314
319
|
get(id: string): Promise<Dispatch | null>;
|
|
315
320
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../../src/dispatch/bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAiB5D,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAKxE,OAAO,KAAK,EACV,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,mBAAmB,EAEnB,sBAAsB,EACtB,oBAAoB,EACpB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAapB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,WAAW,CAAkB;IAErC;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAiB;IAE5D,wEAAwE;IACxE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IAEhD;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAa7B;;OAEG;gBACS,EAAE,EAAE,iBAAiB;IAIjC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAkFjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoDG;IACG,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,QAAQ,CAAC;IA0EpB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI;IAenD;;;;;;OAMG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO;IAcvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACG,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCjE;;;;;OAKG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,OAAO,CACX,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC;IAyIlB;;;;;;;OAOG;IACG,KAAK,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBhE;;;;;OAKG;IACG,OAAO,CACX,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,qBAAqB,CAAC;IAYjC
|
|
1
|
+
{"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../../src/dispatch/bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAiB5D,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAKxE,OAAO,KAAK,EACV,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,mBAAmB,EAEnB,sBAAsB,EACtB,oBAAoB,EACpB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAapB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,WAAW,CAAkB;IAErC;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAiB;IAE5D,wEAAwE;IACxE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IAEhD;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAa7B;;OAEG;gBACS,EAAE,EAAE,iBAAiB;IAIjC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAkFjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoDG;IACG,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,QAAQ,CAAC;IA0EpB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI;IAenD;;;;;;OAMG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO;IAcvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACG,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCjE;;;;;OAKG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,OAAO,CACX,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC;IAyIlB;;;;;;;OAOG;IACG,KAAK,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBhE;;;;;OAKG;IACG,OAAO,CACX,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,qBAAqB,CAAC;IAYjC;;;;;;;;;;;;;;;;;;OAkBG;IACG,IAAI,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAgBlE;;;;;;;;OAQG;IACG,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAU/C;;OAEG;IACG,iBAAiB,CACrB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAkBlC;;OAEG;IACH,OAAO,CAAC,cAAc;CAqCvB;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,WAAW,CAAC,CAwCtB"}
|
package/dist/dispatch/bus.js
CHANGED
|
@@ -455,7 +455,11 @@ var DispatchBus = class DispatchBus {
|
|
|
455
455
|
* tenant or widen visibility to all tenants. Scoping rules:
|
|
456
456
|
*
|
|
457
457
|
* - tenancy off → no tenant filter (pre-tenancy behavior).
|
|
458
|
-
* - active tenant T → that tenant's dispatches
|
|
458
|
+
* - active tenant T → that tenant's dispatches only. Global (NULL-tenant)
|
|
459
|
+
* dispatches are system-only on reads (#3301): a global row's payload (for
|
|
460
|
+
* example a delivery emitted outside tenant context) may carry another
|
|
461
|
+
* tenant's recipients/subjects. Processing (`process`) still lets a tenant
|
|
462
|
+
* worker claim global work.
|
|
459
463
|
* - tenancy on but no active tenant → global (NULL) dispatches only
|
|
460
464
|
* (fail-closed; never all tenants).
|
|
461
465
|
*
|
|
@@ -468,19 +472,20 @@ var DispatchBus = class DispatchBus {
|
|
|
468
472
|
return DispatchCollection.list(this.db, {
|
|
469
473
|
...safeOptions,
|
|
470
474
|
tenantScope: resolveDispatchTenantScope()
|
|
471
|
-
});
|
|
475
|
+
}, false);
|
|
472
476
|
}
|
|
473
477
|
/**
|
|
474
478
|
* Gets a dispatch by ID, enforcing the active tenant scope (S5 #1398).
|
|
475
479
|
*
|
|
476
480
|
* Applies the same server-derived tenant predicate as {@link list}: a
|
|
477
|
-
* subscriber in tenant A cannot fetch tenant B's dispatch
|
|
478
|
-
*
|
|
479
|
-
*
|
|
481
|
+
* subscriber in tenant A cannot fetch tenant B's dispatch, or a global
|
|
482
|
+
* (NULL-tenant) dispatch, by id (#3301), and when tenancy is on with no
|
|
483
|
+
* active tenant only global (NULL) dispatches are returned. Returns `null`
|
|
484
|
+
* when the dispatch exists but is out of scope.
|
|
480
485
|
*/
|
|
481
486
|
async get(id) {
|
|
482
487
|
await this.initialize();
|
|
483
|
-
return DispatchCollection.getScoped(this.db, id, resolveDispatchTenantScope());
|
|
488
|
+
return DispatchCollection.getScoped(this.db, id, resolveDispatchTenantScope(), false);
|
|
484
489
|
}
|
|
485
490
|
/**
|
|
486
491
|
* List all subscriptions
|