@jini-ai/cms 0.5.2 → 0.5.3
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/CHANGELOG.md +11 -0
- package/README.md +14 -8
- package/dist/comments/errors.d.ts +23 -0
- package/dist/comments/errors.js +25 -0
- package/dist/comments/express/index.d.ts +2 -0
- package/dist/comments/express/index.js +2 -0
- package/dist/comments/express/submit-handler.d.ts +30 -0
- package/dist/comments/express/submit-handler.js +85 -0
- package/dist/comments/hooks.d.ts +39 -0
- package/dist/comments/hooks.js +41 -0
- package/dist/comments/index.d.ts +11 -0
- package/dist/comments/index.js +11 -0
- package/dist/comments/ingress.d.ts +57 -0
- package/dist/comments/ingress.js +173 -0
- package/dist/comments/module.d.ts +94 -0
- package/dist/comments/module.js +73 -0
- package/dist/comments/ports.d.ts +184 -0
- package/dist/comments/ports.js +2 -0
- package/dist/comments/repo.memory.d.ts +70 -0
- package/dist/comments/repo.memory.js +120 -0
- package/dist/comments/sanitize.d.ts +15 -0
- package/dist/comments/sanitize.js +31 -0
- package/dist/comments/spam.heuristic.d.ts +8 -0
- package/dist/comments/spam.heuristic.js +54 -0
- package/dist/comments/sql/index.d.ts +3 -0
- package/dist/comments/sql/index.js +3 -0
- package/dist/comments/sql/repo.d.ts +77 -0
- package/dist/comments/sql/repo.js +190 -0
- package/dist/comments/sql/repo.rows.d.ts +49 -0
- package/dist/comments/sql/repo.rows.js +71 -0
- package/dist/comments/types.d.ts +187 -0
- package/dist/comments/types.js +39 -0
- package/dist/comments/write-service.d.ts +121 -0
- package/dist/comments/write-service.js +144 -0
- package/dist/core/commands/command.d.ts +2 -9
- package/dist/core/commands/command.js +2 -9
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +1 -0
- package/dist/core/keyset-cursor.d.ts +25 -0
- package/dist/core/keyset-cursor.js +20 -0
- package/dist/core/ports.d.ts +30 -0
- package/dist/core/ports.js +0 -1
- package/dist/core/tools/args.d.ts +1 -46
- package/dist/core/tools/args.js +1 -44
- package/dist/media/index.d.ts +0 -3
- package/dist/media/index.js +1 -4
- package/dist/media/node.d.ts +5 -0
- package/dist/media/node.js +6 -0
- package/dist/redirects/capture.d.ts +80 -0
- package/dist/redirects/capture.js +66 -0
- package/dist/redirects/express/index.d.ts +27 -0
- package/dist/redirects/express/index.js +40 -0
- package/dist/redirects/hit-sink.d.ts +56 -0
- package/dist/redirects/hit-sink.js +44 -0
- package/dist/redirects/host-config/index.d.ts +40 -0
- package/dist/redirects/host-config/index.js +145 -0
- package/dist/redirects/index.d.ts +14 -0
- package/dist/redirects/index.js +14 -0
- package/dist/redirects/matcher.d.ts +58 -0
- package/dist/redirects/matcher.js +144 -0
- package/dist/redirects/order.d.ts +30 -0
- package/dist/redirects/order.js +26 -0
- package/dist/redirects/ports.d.ts +242 -0
- package/dist/redirects/ports.internal.d.ts +57 -0
- package/dist/redirects/ports.internal.js +14 -0
- package/dist/redirects/ports.js +2 -0
- package/dist/redirects/redirects.d.ts +226 -0
- package/dist/redirects/redirects.js +590 -0
- package/dist/redirects/referrer-alias.d.ts +31 -0
- package/dist/redirects/referrer-alias.js +33 -0
- package/dist/redirects/repo.memory.d.ts +51 -0
- package/dist/redirects/repo.memory.js +98 -0
- package/dist/redirects/reserved-destination.d.ts +58 -0
- package/dist/redirects/reserved-destination.js +59 -0
- package/dist/redirects/resolver.d.ts +100 -0
- package/dist/redirects/resolver.js +87 -0
- package/dist/redirects/spa/index.d.ts +30 -0
- package/dist/redirects/spa/index.js +27 -0
- package/dist/redirects/sql/index.d.ts +4 -0
- package/dist/redirects/sql/index.js +4 -0
- package/dist/redirects/sql/repo.d.ts +82 -0
- package/dist/redirects/sql/repo.js +143 -0
- package/dist/redirects/sql/repo.rows.d.ts +49 -0
- package/dist/redirects/sql/repo.rows.js +79 -0
- package/dist/redirects/sql/row-types.d.ts +34 -0
- package/dist/redirects/sql/row-types.js +2 -0
- package/dist/redirects/target-gate.d.ts +30 -0
- package/dist/redirects/target-gate.js +85 -0
- package/dist/redirects/types.d.ts +213 -0
- package/dist/redirects/types.js +42 -0
- package/dist/seo/absolute-url.d.ts +58 -0
- package/dist/seo/absolute-url.js +117 -0
- package/dist/seo/dom/index.d.ts +14 -0
- package/dist/seo/dom/index.js +52 -0
- package/dist/seo/errors.d.ts +35 -0
- package/dist/seo/errors.js +35 -0
- package/dist/seo/feed.d.ts +49 -0
- package/dist/seo/feed.js +61 -0
- package/dist/seo/index.d.ts +13 -0
- package/dist/seo/index.js +13 -0
- package/dist/seo/media.d.ts +57 -0
- package/dist/seo/media.js +134 -0
- package/dist/seo/page-head.d.ts +18 -0
- package/dist/seo/page-head.js +114 -0
- package/dist/seo/ports.d.ts +221 -0
- package/dist/seo/ports.js +27 -0
- package/dist/seo/seo.d.ts +73 -0
- package/dist/seo/seo.js +230 -0
- package/dist/seo/sitemap.d.ts +139 -0
- package/dist/seo/sitemap.js +217 -0
- package/dist/seo/types.d.ts +278 -0
- package/dist/seo/types.js +2 -0
- package/dist/seo/write-service.d.ts +79 -0
- package/dist/seo/write-service.js +273 -0
- package/dist/seo/xml-escape.d.ts +16 -0
- package/dist/seo/xml-escape.js +19 -0
- package/dist/settings/express/cms-adapter.d.ts +20 -0
- package/dist/settings/express/cms-adapter.js +124 -0
- package/dist/settings/express/contracts.d.ts +135 -0
- package/dist/settings/express/contracts.js +2 -0
- package/dist/settings/express/http.d.ts +10 -0
- package/dist/settings/express/http.js +211 -0
- package/dist/settings/express/index.d.ts +5 -0
- package/dist/settings/express/index.js +5 -0
- package/dist/settings/express/shared.d.ts +68 -0
- package/dist/settings/express/shared.js +54 -0
- package/dist/settings/express/stream.d.ts +24 -0
- package/dist/settings/express/stream.js +147 -0
- package/dist/settings/index.d.ts +3 -38
- package/dist/settings/index.js +3 -35
- package/dist/taxonomy/write-service.d.ts +2 -30
- package/dist/widgets/authorize-helper.d.ts +42 -0
- package/dist/widgets/authorize-helper.js +16 -0
- package/dist/widgets/concurrency.d.ts +12 -0
- package/dist/widgets/concurrency.js +44 -0
- package/dist/widgets/config-validation.d.ts +34 -0
- package/dist/widgets/config-validation.js +125 -0
- package/dist/widgets/embed-service.d.ts +133 -0
- package/dist/widgets/embed-service.js +460 -0
- package/dist/widgets/embed-validation.d.ts +23 -0
- package/dist/widgets/embed-validation.js +38 -0
- package/dist/widgets/entry-payload.d.ts +103 -0
- package/dist/widgets/entry-payload.js +160 -0
- package/dist/widgets/errors.d.ts +136 -0
- package/dist/widgets/errors.js +136 -0
- package/dist/widgets/host-ports.d.ts +149 -0
- package/dist/widgets/host-ports.js +2 -0
- package/dist/widgets/html/html-embeds.d.ts +247 -0
- package/dist/widgets/html/html-embeds.js +408 -0
- package/dist/widgets/html/index.d.ts +5 -0
- package/dist/widgets/html/index.js +4 -0
- package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
- package/dist/widgets/html/legacy-widget-markers.js +45 -0
- package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
- package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +19 -0
- package/dist/widgets/markers/html-attributes.d.ts +98 -0
- package/dist/widgets/markers/html-attributes.js +216 -0
- package/dist/widgets/markers/index.d.ts +3 -0
- package/dist/widgets/markers/index.js +3 -0
- package/dist/widgets/markers/marker.d.ts +496 -0
- package/dist/widgets/markers/marker.js +780 -0
- package/dist/widgets/page-embed-types.d.ts +42 -0
- package/dist/widgets/page-embed-types.js +42 -0
- package/dist/widgets/ports.d.ts +142 -0
- package/dist/widgets/ports.js +2 -0
- package/dist/widgets/read-service.d.ts +81 -0
- package/dist/widgets/read-service.js +79 -0
- package/dist/widgets/region-area-service.d.ts +110 -0
- package/dist/widgets/region-area-service.js +180 -0
- package/dist/widgets/registry.d.ts +217 -0
- package/dist/widgets/registry.js +172 -0
- package/dist/widgets/repo.memory.d.ts +41 -0
- package/dist/widgets/repo.memory.js +48 -0
- package/dist/widgets/resolver-service.d.ts +209 -0
- package/dist/widgets/resolver-service.js +909 -0
- package/dist/widgets/resolvers/contact-form.d.ts +26 -0
- package/dist/widgets/resolvers/contact-form.js +54 -0
- package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
- package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
- package/dist/widgets/resolvers/index.d.ts +65 -0
- package/dist/widgets/resolvers/index.js +155 -0
- package/dist/widgets/resolvers/menu.d.ts +42 -0
- package/dist/widgets/resolvers/menu.js +43 -0
- package/dist/widgets/resolvers/ports.d.ts +82 -0
- package/dist/widgets/resolvers/ports.js +2 -0
- package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
- package/dist/widgets/resolvers/recent-entries.js +165 -0
- package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
- package/dist/widgets/resolvers/taxonomy.js +34 -0
- package/dist/widgets/sql/index.d.ts +3 -0
- package/dist/widgets/sql/index.js +3 -0
- package/dist/widgets/sql/repo.d.ts +41 -0
- package/dist/widgets/sql/repo.js +82 -0
- package/dist/widgets/sql/tables.d.ts +13 -0
- package/dist/widgets/sql/tables.js +2 -0
- package/dist/widgets/types.d.ts +213 -0
- package/dist/widgets/types.js +17 -0
- package/dist/widgets/where-used.d.ts +41 -0
- package/dist/widgets/where-used.js +11 -0
- package/dist/widgets/write-service.d.ts +204 -0
- package/dist/widgets/write-service.js +332 -0
- package/dist/workspace/delete.d.ts +1 -1
- package/package.json +220 -11
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
import type { Authorize, SettingsChangeFeed, SettingsHttpOptions, Scheduler } from "./contracts.js";
|
|
3
|
+
interface StreamRequired {
|
|
4
|
+
request: Request;
|
|
5
|
+
response: Response;
|
|
6
|
+
workspaceId: string;
|
|
7
|
+
principalId: string;
|
|
8
|
+
changeFeed: SettingsChangeFeed;
|
|
9
|
+
authorize: Authorize;
|
|
10
|
+
permission: string;
|
|
11
|
+
scheduler: Scheduler;
|
|
12
|
+
onClose: (required: {
|
|
13
|
+
close: () => void;
|
|
14
|
+
}) => void;
|
|
15
|
+
isDisposed: () => boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Starts after HTTP authorization. All scheduling and storage are injected. */
|
|
18
|
+
/** Poll with bounded pages, tenant-scoped ids and retryable reads; close cancels all schedules.
|
|
19
|
+
* O(page size) per tick and O(1) connection state; repository effects live behind the feed port.
|
|
20
|
+
* Invalid scheduler intervals/page sizes throw RangeError before opening; initial read errors propagate.
|
|
21
|
+
*/
|
|
22
|
+
export declare function openSettingsChangeStream(required: StreamRequired, optional?: SettingsHttpOptions): Promise<() => void>;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=stream.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Starts after HTTP authorization. All scheduling and storage are injected. */
|
|
2
|
+
/** Poll with bounded pages, tenant-scoped ids and retryable reads; close cancels all schedules.
|
|
3
|
+
* O(page size) per tick and O(1) connection state; repository effects live behind the feed port.
|
|
4
|
+
* Invalid scheduler intervals/page sizes throw RangeError before opening; initial read errors propagate.
|
|
5
|
+
*/
|
|
6
|
+
export async function openSettingsChangeStream(required, optional = {}) {
|
|
7
|
+
const { request, response, workspaceId, principalId, changeFeed, scheduler } = required;
|
|
8
|
+
const pageSize = optional.revisionPageSize ?? 200;
|
|
9
|
+
const intervals = [optional.pollIntervalMs ?? 1_000, optional.keepaliveIntervalMs ?? 25_000, optional.reauthorizeIntervalMs ?? 30_000];
|
|
10
|
+
if (!Number.isSafeInteger(pageSize) || pageSize <= 0 || intervals.some((n) => !Number.isFinite(n) || n <= 0)) {
|
|
11
|
+
throw new RangeError("settings feed page size and intervals must be positive");
|
|
12
|
+
}
|
|
13
|
+
let closed = false;
|
|
14
|
+
let ticking = false;
|
|
15
|
+
let checkingAuthorization = false;
|
|
16
|
+
const cancellations = [];
|
|
17
|
+
const reportError = (operation, error) => {
|
|
18
|
+
// Reporting is best-effort: a host hook must never turn recovery into an unhandled rejection.
|
|
19
|
+
try {
|
|
20
|
+
if (optional.onError)
|
|
21
|
+
optional.onError({ operation, error });
|
|
22
|
+
else
|
|
23
|
+
console.error(`[settings] change feed ${operation} failed`, error);
|
|
24
|
+
}
|
|
25
|
+
catch { /* Diagnostic failures cannot interrupt stream cleanup or retries. */ }
|
|
26
|
+
};
|
|
27
|
+
const recover = (operation, effect) => {
|
|
28
|
+
try {
|
|
29
|
+
effect();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
reportError(operation, error);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const release = () => {
|
|
36
|
+
// Every resource gets its own recovery boundary so one failing cancellation cannot leak others.
|
|
37
|
+
for (const cancel of cancellations.splice(0))
|
|
38
|
+
recover("cancel", cancel);
|
|
39
|
+
recover("request-listener", () => { request.off("close", close); });
|
|
40
|
+
recover("response-listener", () => { response.off("close", close); });
|
|
41
|
+
};
|
|
42
|
+
const close = () => {
|
|
43
|
+
if (closed)
|
|
44
|
+
return;
|
|
45
|
+
closed = true;
|
|
46
|
+
release();
|
|
47
|
+
recover("close", () => { required.onClose({ close }); });
|
|
48
|
+
if (!response.writableEnded && !response.destroyed)
|
|
49
|
+
recover("end", () => { response.end(); });
|
|
50
|
+
};
|
|
51
|
+
request.on("close", close);
|
|
52
|
+
response.on("close", close);
|
|
53
|
+
try {
|
|
54
|
+
const head = await changeFeed.head({});
|
|
55
|
+
if (closed || required.isDisposed() || response.destroyed || response.writableEnded) {
|
|
56
|
+
close();
|
|
57
|
+
return close;
|
|
58
|
+
}
|
|
59
|
+
const resumeFrom = Number(request.headers["last-event-id"]);
|
|
60
|
+
let cursor = Number.isFinite(resumeFrom) && resumeFrom >= 0 ? Math.min(resumeFrom, head) : head;
|
|
61
|
+
let lastEmittedId = cursor;
|
|
62
|
+
const viewer = { workspaceId, principalId };
|
|
63
|
+
response.writeHead(200, {
|
|
64
|
+
"Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform",
|
|
65
|
+
...(request.httpVersionMajor < 2 ? { Connection: "keep-alive" } : {}), "X-Accel-Buffering": "no",
|
|
66
|
+
});
|
|
67
|
+
const tick = async () => {
|
|
68
|
+
if (closed || ticking)
|
|
69
|
+
return;
|
|
70
|
+
ticking = true;
|
|
71
|
+
try {
|
|
72
|
+
// Snapshot head before the page: concurrent writes must never be skipped.
|
|
73
|
+
const ledgerHead = await changeFeed.head({});
|
|
74
|
+
if (closed)
|
|
75
|
+
return;
|
|
76
|
+
const batch = await changeFeed.collect({ sinceSeq: cursor, limit: pageSize, viewer });
|
|
77
|
+
if (closed)
|
|
78
|
+
return;
|
|
79
|
+
const examined = batch.examinedCount < pageSize ? ledgerHead : batch.cursor;
|
|
80
|
+
cursor = Math.max(cursor, examined, batch.cursor);
|
|
81
|
+
if (!batch.namespaces.length)
|
|
82
|
+
return;
|
|
83
|
+
// The internal global head is never disclosed as an event id.
|
|
84
|
+
lastEmittedId = Math.max(lastEmittedId, batch.cursor);
|
|
85
|
+
response.write(`id: ${lastEmittedId}\nevent: settings-changed\ndata: ${JSON.stringify({ namespaces: batch.namespaces })}\n\n`);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (!closed)
|
|
89
|
+
reportError("poll", error);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
ticking = false;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const reauthorize = async () => {
|
|
96
|
+
if (closed || checkingAuthorization)
|
|
97
|
+
return;
|
|
98
|
+
checkingAuthorization = true;
|
|
99
|
+
try {
|
|
100
|
+
const result = await required.authorize({ principalId, workspaceId, permission: required.permission, entityType: "setting-value" });
|
|
101
|
+
if (!closed && !result.allowed)
|
|
102
|
+
close();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (!closed)
|
|
106
|
+
reportError("re-authorization", error);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
checkingAuthorization = false;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const schedule = (intervalMs, callback) => {
|
|
113
|
+
const cancel = scheduler.every({ intervalMs, callback });
|
|
114
|
+
if (closed)
|
|
115
|
+
recover("cancel", cancel);
|
|
116
|
+
else
|
|
117
|
+
cancellations.push(cancel);
|
|
118
|
+
};
|
|
119
|
+
schedule(intervals[0], () => { void tick(); });
|
|
120
|
+
schedule(intervals[1], () => {
|
|
121
|
+
if (closed)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
response.write(": keepalive\n\n");
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
reportError("keepalive", error);
|
|
128
|
+
close();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
schedule(intervals[2], () => { void reauthorize(); });
|
|
132
|
+
if (!closed)
|
|
133
|
+
response.write(": connected\n\n");
|
|
134
|
+
return close;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
// Cancel schedules even when opening a feed fails. Let the HTTP wrapper map the error.
|
|
138
|
+
if (response.headersSent)
|
|
139
|
+
close();
|
|
140
|
+
else {
|
|
141
|
+
closed = true;
|
|
142
|
+
release();
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=stream.js.map
|
package/dist/settings/index.d.ts
CHANGED
|
@@ -1,40 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* A module's public contract is its `index.ts` — deep imports from outside
|
|
5
|
-
* this directory should go through here.
|
|
6
|
-
*
|
|
7
|
-
* A host's `repo.sqlite.ts` adapter and any one-time brownfield migration of a
|
|
8
|
-
* host's own legacy pre-ledger settings table are NOT part of this package —
|
|
9
|
-
* a fresh host has no legacy data to migrate, and a SQLite adapter names a
|
|
10
|
-
* host's own database schema. Both stay host-side, composed against the
|
|
11
|
-
* `SettingsRepoPort` this barrel exports.
|
|
1
|
+
/** @deprecated For one compatibility release: settings are generic and owned by core.
|
|
2
|
+
* Re-export the owner so published consumers retain function, error and cache identities.
|
|
12
3
|
*/
|
|
13
|
-
export
|
|
14
|
-
export { DefinitionInvalidError, ScopeNotAllowedError, SecretNotSupportedError, ValueValidationFailedError, RenameRetypeConflictError, AliasDepthExceededError, DefinitionTombstonedError, DefinitionNotFoundError, PurgeRequiredError, ForbiddenError, PrincipalNotFoundError, } from "./errors.js";
|
|
15
|
-
export type { SettingsRepoPort } from "./ports.js";
|
|
16
|
-
export { InMemorySettingsRepo } from "./repo.memory.js";
|
|
17
|
-
export { type DefinitionInput, validateDefinitionInput, validateValueAgainstSchema, registerCoercer, invalidateDefinitionNamespaceCache, invalidateWorkspaceSettingsCache, resolveDefinitionRaw, resolveDefinition, type ResolvedSetting, getEffective, } from "./settings.js";
|
|
18
|
-
export { type AuthorizeFn, type SettingsWriteServiceDeps, deriveRequiredPermission, type RegisterDefinitionsRequired, registerDefinitions, type SetValueRequired, set, type ClearValueRequired, clear, type ResetNamespaceRequired, resetNamespace, type RenameDefinitionRequired, renameDefinition, type RetypeDefinitionRequired, retypeDefinition, type ReconcileDefinitionDefaultRequired, reconcileDefinitionDefault, type DeprecateDefinitionRequired, deprecateDefinition, type TombstoneDefinitionRequired, tombstoneDefinition, } from "./write-service.js";
|
|
19
|
-
export { type PurgeServiceDeps, type PurgeTenantSettingsRequired, purgeTenantSettings, } from "./purge-service.js";
|
|
20
|
-
export { type ChangeFeedViewer, isRevisionVisibleTo, type ChangeFeedBatch, collectChangedNamespaces, } from "./change-feed.js";
|
|
21
|
-
export { type DefinitionOpRequestItem, type DefinitionOpContext, type DefinitionOpHandler, NON_REGISTER_DEFINITION_OP_NAMES, type NonRegisterDefinitionOp, parseNonRegisterDefinitionOp, NON_REGISTER_DEFINITION_OPS, } from "./definitions-dispatch.js";
|
|
22
|
-
export { type SettingDefinitionSpec, type EnsureSettingDefinitionsDeps, type EnsureSettingDefinitionsInput, ensureSettingDefinitions, } from "./ensure-definitions.js";
|
|
23
|
-
export { INSTRUCTIONS_NAMESPACE, NOTIFICATIONS_NAMESPACE, PRIVACY_NAMESPACE, APPEARANCE_NAMESPACE, LANGUAGE_NAMESPACE, type EnsureSettingsUiTabDefinitionsInput, ensureSettingsUiTabDefinitions, } from "./ui-tab-definitions.js";
|
|
24
|
-
export { type AgentWritablePreference, AGENT_PREFERENCE_WRITE_SCOPE, AGENT_WRITABLE_PREFERENCES, AGENT_WRITABLE_PREFERENCE_IDS, resolveAgentWritablePreference, AGENT_PREFERENCE_REQUIRED_SCOPE_BIT, } from "./agent-writable-preferences.js";
|
|
25
|
-
/** The agent-tool catalog for this domain. */
|
|
26
|
-
export { getSettingsAgentToolCatalog, } from "./agent-tools.js";
|
|
27
|
-
/**
|
|
28
|
-
* The agent-tool wiring for this domain.
|
|
29
|
-
*
|
|
30
|
-
* `SettingsToolDeps` is declared structurally rather than derived from any host's request-scoped
|
|
31
|
-
* dependency bag, which is what lets a host satisfy it by passing whatever object it already has —
|
|
32
|
-
* the shape is the contract, so no host type needs to be named here.
|
|
33
|
-
*/
|
|
34
|
-
export { buildSettingsRegistrations, settingsDerivedRisk, type SettingsToolDeps, } from "./tool-registrations.js";
|
|
35
|
-
/** The settings-dialog chrome copy (24 generic tab labels/subtitles), shared across every host. */
|
|
36
|
-
export { SETTINGS_DIALOG_DICTIONARIES, translateSettingsDialog } from "./dictionaries/index.js";
|
|
37
|
-
export { AGENT_WRITE_CONFIRMATION_SETTINGS, AGENT_WRITE_DENIED_SETTINGS, findAgentWriteRule, type AgentSettingWriteRule } from "./agent-write-denylist.js";
|
|
38
|
-
export type { SettingsWriteConfirmation, SettingsValueWriteToolId } from "./agent-value-write-tools.js";
|
|
39
|
-
export type { SettingsPrincipalLookupPort } from "./principal-lookup.js";
|
|
4
|
+
export * from "@jini-ai/core/settings";
|
|
40
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/settings/index.js
CHANGED
|
@@ -1,37 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* A module's public contract is its `index.ts` — deep imports from outside
|
|
5
|
-
* this directory should go through here.
|
|
6
|
-
*
|
|
7
|
-
* A host's `repo.sqlite.ts` adapter and any one-time brownfield migration of a
|
|
8
|
-
* host's own legacy pre-ledger settings table are NOT part of this package —
|
|
9
|
-
* a fresh host has no legacy data to migrate, and a SQLite adapter names a
|
|
10
|
-
* host's own database schema. Both stay host-side, composed against the
|
|
11
|
-
* `SettingsRepoPort` this barrel exports.
|
|
1
|
+
/** @deprecated For one compatibility release: settings are generic and owned by core.
|
|
2
|
+
* Re-export the owner so published consumers retain function, error and cache identities.
|
|
12
3
|
*/
|
|
13
|
-
export
|
|
14
|
-
export { DefinitionInvalidError, ScopeNotAllowedError, SecretNotSupportedError, ValueValidationFailedError, RenameRetypeConflictError, AliasDepthExceededError, DefinitionTombstonedError, DefinitionNotFoundError, PurgeRequiredError, ForbiddenError, PrincipalNotFoundError, } from "./errors.js";
|
|
15
|
-
export { InMemorySettingsRepo } from "./repo.memory.js";
|
|
16
|
-
export { validateDefinitionInput, validateValueAgainstSchema, registerCoercer, invalidateDefinitionNamespaceCache, invalidateWorkspaceSettingsCache, resolveDefinitionRaw, resolveDefinition, getEffective, } from "./settings.js";
|
|
17
|
-
export { deriveRequiredPermission, registerDefinitions, set, clear, resetNamespace, renameDefinition, retypeDefinition, reconcileDefinitionDefault, deprecateDefinition, tombstoneDefinition, } from "./write-service.js";
|
|
18
|
-
export { purgeTenantSettings, } from "./purge-service.js";
|
|
19
|
-
export { isRevisionVisibleTo, collectChangedNamespaces, } from "./change-feed.js";
|
|
20
|
-
export { NON_REGISTER_DEFINITION_OP_NAMES, parseNonRegisterDefinitionOp, NON_REGISTER_DEFINITION_OPS, } from "./definitions-dispatch.js";
|
|
21
|
-
export { ensureSettingDefinitions, } from "./ensure-definitions.js";
|
|
22
|
-
export { INSTRUCTIONS_NAMESPACE, NOTIFICATIONS_NAMESPACE, PRIVACY_NAMESPACE, APPEARANCE_NAMESPACE, LANGUAGE_NAMESPACE, ensureSettingsUiTabDefinitions, } from "./ui-tab-definitions.js";
|
|
23
|
-
export { AGENT_PREFERENCE_WRITE_SCOPE, AGENT_WRITABLE_PREFERENCES, AGENT_WRITABLE_PREFERENCE_IDS, resolveAgentWritablePreference, AGENT_PREFERENCE_REQUIRED_SCOPE_BIT, } from "./agent-writable-preferences.js";
|
|
24
|
-
/** The agent-tool catalog for this domain. */
|
|
25
|
-
export { getSettingsAgentToolCatalog, } from "./agent-tools.js";
|
|
26
|
-
/**
|
|
27
|
-
* The agent-tool wiring for this domain.
|
|
28
|
-
*
|
|
29
|
-
* `SettingsToolDeps` is declared structurally rather than derived from any host's request-scoped
|
|
30
|
-
* dependency bag, which is what lets a host satisfy it by passing whatever object it already has —
|
|
31
|
-
* the shape is the contract, so no host type needs to be named here.
|
|
32
|
-
*/
|
|
33
|
-
export { buildSettingsRegistrations, settingsDerivedRisk, } from "./tool-registrations.js";
|
|
34
|
-
/** The settings-dialog chrome copy (24 generic tab labels/subtitles), shared across every host. */
|
|
35
|
-
export { SETTINGS_DIALOG_DICTIONARIES, translateSettingsDialog } from "./dictionaries/index.js";
|
|
36
|
-
export { AGENT_WRITE_CONFIRMATION_SETTINGS, AGENT_WRITE_DENIED_SETTINGS, findAgentWriteRule } from "./agent-write-denylist.js";
|
|
4
|
+
export * from "@jini-ai/core/settings";
|
|
37
5
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { TransactionalRepoPort } from "../core/ports.js";
|
|
2
|
+
export type { TransactionalRepoPort } from "../core/ports.js";
|
|
1
3
|
import type { Clock, IdGenerator } from "@jini-ai/core/primitives";
|
|
2
4
|
import type { TermListPort } from "./list.js";
|
|
3
5
|
/**
|
|
@@ -459,36 +461,6 @@ export declare class TaxonomyHasAssignedContentError extends Error {
|
|
|
459
461
|
assignedCount: number;
|
|
460
462
|
}, _optionalArgs?: Record<string, never>);
|
|
461
463
|
}
|
|
462
|
-
/**
|
|
463
|
-
* Opens/commits/rolls back one transaction around `fn` — the same shape and contract as this
|
|
464
|
-
* codebase's other write chokepoints that need one (`RedirectsWriteDeps.transaction`,
|
|
465
|
-
* `SqliteSettingsRepo.transaction`).
|
|
466
|
-
*
|
|
467
|
-
* `deleteTerm`/`deleteTaxonomy` run their ENTIRE body — every guard read (`findById`,
|
|
468
|
-
* `countChildren`, `countByTerm`) AND every write (`delete`, `revisions.insert`, `outbox.enqueue`)
|
|
469
|
-
* — inside one call to this. Two failure modes this closes that a guard-then-transaction split
|
|
470
|
-
* would not:
|
|
471
|
-
*
|
|
472
|
-
* 1. **TOCTOU on the guard.** A guard read taken BEFORE the transaction opens can go stale the
|
|
473
|
-
* instant it returns — content can be assigned to the term in the gap between the read and the
|
|
474
|
-
* delete, and the delete would proceed having checked a now-wrong count. Putting the read
|
|
475
|
-
* inside the same transaction as the delete is what makes the guard's answer still true at the
|
|
476
|
-
* moment the delete actually runs.
|
|
477
|
-
* 2. **Partial cascade.** `deleteTaxonomy` deletes N member terms then the taxonomy row itself,
|
|
478
|
-
* with no FK/CASCADE at the schema level (`db/schema.ts` — `taxonomies`/`terms`/`entry_terms`/
|
|
479
|
-
* `taxonomy_revisions` are plain columns, no foreign keys) to undo a mid-cascade failure. A
|
|
480
|
-
* real transaction is the only thing that can.
|
|
481
|
-
*
|
|
482
|
-
* A caller-supplied identity/no-op implementation is a valid choice for a backend that has no
|
|
483
|
-
* separate transaction primitive to offer (see `repo.memory.ts`'s `InMemoryTaxonomyRepo` for the
|
|
484
|
-
* disclosed rationale) — the port exists so `deleteTerm`/`deleteTaxonomy` never have to know which
|
|
485
|
-
* kind of backend they're running against.
|
|
486
|
-
*/
|
|
487
|
-
export interface TransactionalRepoPort {
|
|
488
|
-
transaction<T>({ fn }: {
|
|
489
|
-
fn: () => Promise<T>;
|
|
490
|
-
}): Promise<T>;
|
|
491
|
-
}
|
|
492
464
|
export interface DeleteTermRequired {
|
|
493
465
|
deps: WriteServiceDeps & {
|
|
494
466
|
terms: DeletableTermRepoPort;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Shared `authorize()` plumbing for `write-service.ts`/`region-area-service.ts` (SPEC-043
|
|
3
|
+
* REQ-40/41, INV-07).
|
|
4
|
+
*
|
|
5
|
+
* Purpose:
|
|
6
|
+
* `AuthorizeFn` mirrors `features/entries/write-service.ts`'s `AuthorizeFn` shape structurally (no
|
|
7
|
+
* shared import, kept decoupled — the same "no shared import" convention that file's own header
|
|
8
|
+
* documents against `features/content-types`). `requireWidgetPermission` is the ONE place a
|
|
9
|
+
* `widgets.*` permission string gets checked before a mutation proceeds.
|
|
10
|
+
*
|
|
11
|
+
* `PRE_AUTHORIZED` exists because this library composes `features/entries/write-service.ts`'s
|
|
12
|
+
* `createEntry`/`updateEntry` and `features/content-types/write-service.ts`'s `registerContentType`
|
|
13
|
+
* — both of which perform their OWN internal `authorize()` call, hardcoded to the
|
|
14
|
+
* `admin.collections.manage` permission (the generic Collections-domain permission, unrelated to
|
|
15
|
+
* widgets' own `widgets.*` model, and not modifiable — see this task's scope boundary on
|
|
16
|
+
* `features/entries`). Since the real, authoritative check for a widget mutation is the
|
|
17
|
+
* `widgets.*` check `requireWidgetPermission` already performed one layer up, `PRE_AUTHORIZED` is
|
|
18
|
+
* passed as those composed functions' own `authorize` dependency so their internal check is not a
|
|
19
|
+
* second, differently-scoped gate a legitimate `widgets.*`-holding caller could be incorrectly
|
|
20
|
+
* rejected by.
|
|
21
|
+
*/
|
|
22
|
+
export type WidgetsAuthorizeFn = (params: {
|
|
23
|
+
principalId: string;
|
|
24
|
+
permission: string;
|
|
25
|
+
workspaceId: string;
|
|
26
|
+
}) => Promise<{
|
|
27
|
+
allowed: boolean;
|
|
28
|
+
reason: string;
|
|
29
|
+
}>;
|
|
30
|
+
export declare function requireWidgetPermission(required: {
|
|
31
|
+
authorize: WidgetsAuthorizeFn;
|
|
32
|
+
actor: {
|
|
33
|
+
principalId: string;
|
|
34
|
+
};
|
|
35
|
+
workspaceId: string;
|
|
36
|
+
permission: string;
|
|
37
|
+
}, _optional?: Record<string, never>): Promise<void>;
|
|
38
|
+
/** See file header — passed as the composed entries/content-types chokepoints' own `authorize` dep. */
|
|
39
|
+
export declare const PRE_AUTHORIZED: WidgetsAuthorizeFn;
|
|
40
|
+
/** The actor id attributed to fully-automatic, system-driven writes (content-type seeding, theme-activation region seeding — REQ-13). Mirrors `server/seed.ts`'s `SETTINGS_MIGRATION_SYSTEM_PRINCIPAL_ID` convention. */
|
|
41
|
+
export declare const WIDGETS_SYSTEM_ACTOR_ID = "system-widgets";
|
|
42
|
+
//# sourceMappingURL=authorize-helper.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { WidgetForbiddenError } from "./errors.js";
|
|
2
|
+
export async function requireWidgetPermission(required, _optional = {}) {
|
|
3
|
+
const { authorize, actor, workspaceId, permission } = required;
|
|
4
|
+
const result = await authorize({ principalId: actor.principalId, permission, workspaceId });
|
|
5
|
+
if (!result.allowed) {
|
|
6
|
+
throw new WidgetForbiddenError({ message: `principal '${actor.principalId}' lacks permission '${permission}' (${result.reason})` });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** See file header — passed as the composed entries/content-types chokepoints' own `authorize` dep. */
|
|
10
|
+
export const PRE_AUTHORIZED = async () => ({
|
|
11
|
+
allowed: true,
|
|
12
|
+
reason: "widgets: authorized upstream via the widgets.* permission check",
|
|
13
|
+
});
|
|
14
|
+
/** The actor id attributed to fully-automatic, system-driven writes (content-type seeding, theme-activation region seeding — REQ-13). Mirrors `server/seed.ts`'s `SETTINGS_MIGRATION_SYSTEM_PRINCIPAL_ID` convention. */
|
|
15
|
+
export const WIDGETS_SYSTEM_ACTOR_ID = "system-widgets";
|
|
16
|
+
//# sourceMappingURL=authorize-helper.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs `fn` exclusively with respect to every other `withEntryLock` call sharing the same `key` —
|
|
3
|
+
* callers queue in call order, each waiting for the previous holder to finish.
|
|
4
|
+
*
|
|
5
|
+
* @complexity O(1) scheduling overhead beyond `fn`'s own cost.
|
|
6
|
+
* @overallScore 100
|
|
7
|
+
*/
|
|
8
|
+
export declare function withEntryLock<T>({ key, fn }: {
|
|
9
|
+
key: string;
|
|
10
|
+
fn: () => Promise<T>;
|
|
11
|
+
}, _optional?: Record<string, never>): Promise<T>;
|
|
12
|
+
//# sourceMappingURL=concurrency.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A tiny per-key async mutex (SPEC-043 REQ-06/15, AC-04/EC-02).
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* `features/entries/write-service.ts`'s `updateEntry` reads the current row, checks
|
|
6
|
+
* `expectedVersion`, then writes — but `InMemoryEntryRepo.transaction` provides no real
|
|
7
|
+
* serialization (`async transaction(fn) { return fn(); }`), and the version check itself happens
|
|
8
|
+
* BEFORE that transaction call. Two truly concurrent callers racing the same entry id can both
|
|
9
|
+
* observe the pre-write version and both pass the check under the in-memory adapter (the real
|
|
10
|
+
* `SqliteEntryRepo.transaction`'s `BEGIN IMMEDIATE` already serializes real concurrent writers at
|
|
11
|
+
* the DB layer — this mutex is redundant-but-harmless there, and load-bearing for the in-memory
|
|
12
|
+
* path this test suite's concurrency assertions exercise).
|
|
13
|
+
*
|
|
14
|
+
* This module serializes the whole "read current version -> validate -> call updateEntry" sequence
|
|
15
|
+
* per entry id at the WIDGETS domain layer, restoring correct optimistic-concurrency semantics
|
|
16
|
+
* deterministically regardless of the underlying repo adapter's own concurrency properties.
|
|
17
|
+
*
|
|
18
|
+
* Architectural role:
|
|
19
|
+
* `widgets` domain-internal helper, not a frozen public contract.
|
|
20
|
+
*/
|
|
21
|
+
const locks = new Map();
|
|
22
|
+
/**
|
|
23
|
+
* Runs `fn` exclusively with respect to every other `withEntryLock` call sharing the same `key` —
|
|
24
|
+
* callers queue in call order, each waiting for the previous holder to finish.
|
|
25
|
+
*
|
|
26
|
+
* @complexity O(1) scheduling overhead beyond `fn`'s own cost.
|
|
27
|
+
* @overallScore 100
|
|
28
|
+
*/
|
|
29
|
+
export async function withEntryLock({ key, fn }, _optional = {}) {
|
|
30
|
+
const previous = locks.get(key) ?? Promise.resolve();
|
|
31
|
+
let release;
|
|
32
|
+
const next = new Promise((resolve) => {
|
|
33
|
+
release = resolve;
|
|
34
|
+
});
|
|
35
|
+
locks.set(key, previous.then(() => next));
|
|
36
|
+
await previous;
|
|
37
|
+
try {
|
|
38
|
+
return await fn();
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
release();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=concurrency.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface WidgetConfigFieldError {
|
|
2
|
+
readonly field: string;
|
|
3
|
+
readonly reason: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ValidateWidgetConfigResult {
|
|
6
|
+
readonly valid: boolean;
|
|
7
|
+
readonly fieldErrors: WidgetConfigFieldError[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Validates `config` against a widget type's registered `configSchema` (REQ-02). The registered
|
|
11
|
+
* schema is always the authority — a type's own `clamps` (e.g. `recent-entries`' `maxItems: 20`)
|
|
12
|
+
* are already expressed as `maximum` constraints inside the schema itself (`registry.ts`), so no
|
|
13
|
+
* separate clamp check is needed here to satisfy AC-02.
|
|
14
|
+
*
|
|
15
|
+
* @complexity O(n) over the config bag's total node count.
|
|
16
|
+
* @overallScore 100
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateWidgetConfig(required: {
|
|
19
|
+
schema: Record<string, unknown>;
|
|
20
|
+
config: Record<string, unknown>;
|
|
21
|
+
}, _optional?: Record<string, never>): ValidateWidgetConfigResult;
|
|
22
|
+
/**
|
|
23
|
+
* Every free-text string `config` holds where its type's `configSchema` declares one — a `text`
|
|
24
|
+
* widget's `body`, each social link's `url`. Ref fields (`x-ref-target`) are left out (they name
|
|
25
|
+
* other content by id) and so is anything the schema does not declare (`recent-entries`' `where`).
|
|
26
|
+
* The publish carry-along (`publish-content.ts`) reads media URLs out of these.
|
|
27
|
+
*
|
|
28
|
+
* @complexity O(n) over the config's declared nodes.
|
|
29
|
+
*/
|
|
30
|
+
export declare function collectWidgetConfigStrings(required: {
|
|
31
|
+
schema: Record<string, unknown>;
|
|
32
|
+
config: Record<string, unknown>;
|
|
33
|
+
}, _optional?: Record<string, never>): string[];
|
|
34
|
+
//# sourceMappingURL=config-validation.d.ts.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { isNonArrayRecord } from "@jini-ai/core";
|
|
2
|
+
/** Reports every key in `schema.required` that is missing from `value`. */
|
|
3
|
+
function validateRequiredKeys(value, schema, path, errors) {
|
|
4
|
+
for (const key of schema.required ?? []) {
|
|
5
|
+
if (!(key in value))
|
|
6
|
+
errors.push({ field: `${path}.${key}`, reason: "required field is missing" });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Recurses into each `value` key that has a declared `schema.properties` entry; flags the rest when `additionalProperties` is `false`. */
|
|
10
|
+
function walkObjectProperties(value, schema, path, errors) {
|
|
11
|
+
for (const [key, fieldValue] of Object.entries(value)) {
|
|
12
|
+
const propSchema = schema.properties?.[key];
|
|
13
|
+
if (!propSchema) {
|
|
14
|
+
if (schema.additionalProperties === false) {
|
|
15
|
+
errors.push({ field: `${path}.${key}`, reason: "unrecognized field: not present in the registered schema" });
|
|
16
|
+
}
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
walk(fieldValue, propSchema, `${path}.${key}`, errors);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Checks `required` keys are present and recurses into each declared property (schema.type === "object"). */
|
|
23
|
+
function walkObject(value, schema, path, errors) {
|
|
24
|
+
const record = { value };
|
|
25
|
+
if (!isNonArrayRecord(record)) {
|
|
26
|
+
errors.push({ field: path, reason: "expected an object" });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
validateRequiredKeys(record.value, schema, path, errors);
|
|
30
|
+
walkObjectProperties(record.value, schema, path, errors);
|
|
31
|
+
}
|
|
32
|
+
/** schema.type === "string": type check only, no length/pattern constraint in this subset. */
|
|
33
|
+
function walkString(value, path, errors) {
|
|
34
|
+
if (typeof value !== "string")
|
|
35
|
+
errors.push({ field: path, reason: "expected a string" });
|
|
36
|
+
}
|
|
37
|
+
/** schema.type === "integer": type check plus optional `minimum`/`maximum` bounds. */
|
|
38
|
+
function walkInteger(value, schema, path, errors) {
|
|
39
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
40
|
+
errors.push({ field: path, reason: "expected an integer" });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
44
|
+
errors.push({ field: path, reason: `below the minimum of ${schema.minimum}` });
|
|
45
|
+
}
|
|
46
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
47
|
+
errors.push({ field: path, reason: `above the maximum of ${schema.maximum}` });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** schema.type === "array": type check, optional `maxItems` bound, and recursion into `items`. */
|
|
51
|
+
function walkArray(value, schema, path, errors) {
|
|
52
|
+
if (!Array.isArray(value)) {
|
|
53
|
+
errors.push({ field: path, reason: "expected an array" });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
57
|
+
errors.push({ field: path, reason: `exceeds the maximum item count of ${schema.maxItems}` });
|
|
58
|
+
}
|
|
59
|
+
if (schema.items) {
|
|
60
|
+
value.forEach((item, index) => {
|
|
61
|
+
walk(item, schema.items, `${path}[${index}]`, errors);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Dispatches to the per-type walker for `schema.type` (object/string/integer/array); unspecified or boolean types accept anything. */
|
|
66
|
+
function walk(value, schema, path, errors) {
|
|
67
|
+
switch (schema.type) {
|
|
68
|
+
case "object":
|
|
69
|
+
walkObject(value, schema, path, errors);
|
|
70
|
+
return;
|
|
71
|
+
case "string":
|
|
72
|
+
walkString(value, path, errors);
|
|
73
|
+
return;
|
|
74
|
+
case "integer":
|
|
75
|
+
walkInteger(value, schema, path, errors);
|
|
76
|
+
return;
|
|
77
|
+
case "array":
|
|
78
|
+
walkArray(value, schema, path, errors);
|
|
79
|
+
return;
|
|
80
|
+
default:
|
|
81
|
+
// boolean / unspecified schema type: accept anything (no v1 registration needs this).
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Validates `config` against a widget type's registered `configSchema` (REQ-02). The registered
|
|
87
|
+
* schema is always the authority — a type's own `clamps` (e.g. `recent-entries`' `maxItems: 20`)
|
|
88
|
+
* are already expressed as `maximum` constraints inside the schema itself (`registry.ts`), so no
|
|
89
|
+
* separate clamp check is needed here to satisfy AC-02.
|
|
90
|
+
*
|
|
91
|
+
* @complexity O(n) over the config bag's total node count.
|
|
92
|
+
* @overallScore 100
|
|
93
|
+
*/
|
|
94
|
+
export function validateWidgetConfig(required, _optional = {}) {
|
|
95
|
+
const errors = [];
|
|
96
|
+
walk(required.config, required.schema, "config", errors);
|
|
97
|
+
return { valid: errors.length === 0, fieldErrors: errors };
|
|
98
|
+
}
|
|
99
|
+
/** Pushes each declared free-text string under `schema` (recursion into declared `properties` and
|
|
100
|
+
* `items` only). @complexity O(n) over the config's declared nodes. */
|
|
101
|
+
function collectStrings(value, schema, out) {
|
|
102
|
+
const record = { value };
|
|
103
|
+
if (schema.type === "string" && typeof value === "string" && schema["x-ref-target"] === undefined)
|
|
104
|
+
out.push(value);
|
|
105
|
+
else if (schema.type === "array" && Array.isArray(value) && schema.items)
|
|
106
|
+
for (const item of value)
|
|
107
|
+
collectStrings(item, schema.items, out);
|
|
108
|
+
else if (schema.type === "object" && isNonArrayRecord(record))
|
|
109
|
+
for (const [key, propSchema] of Object.entries(schema.properties ?? {}))
|
|
110
|
+
collectStrings(record.value[key], propSchema, out);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Every free-text string `config` holds where its type's `configSchema` declares one — a `text`
|
|
114
|
+
* widget's `body`, each social link's `url`. Ref fields (`x-ref-target`) are left out (they name
|
|
115
|
+
* other content by id) and so is anything the schema does not declare (`recent-entries`' `where`).
|
|
116
|
+
* The publish carry-along (`publish-content.ts`) reads media URLs out of these.
|
|
117
|
+
*
|
|
118
|
+
* @complexity O(n) over the config's declared nodes.
|
|
119
|
+
*/
|
|
120
|
+
export function collectWidgetConfigStrings(required, _optional = {}) {
|
|
121
|
+
const out = [];
|
|
122
|
+
collectStrings(required.config, required.schema, out);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=config-validation.js.map
|