@happyvertical/smrt-content 0.43.9 → 0.44.0

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.
Files changed (145) hide show
  1. package/agents/content-list.md +69 -0
  2. package/dist/chunks/content-corrections-BxfA3tW1.js +107 -0
  3. package/dist/chunks/content-corrections-BxfA3tW1.js.map +1 -0
  4. package/dist/chunks/content-query-B4cHfrig.js +4092 -0
  5. package/dist/chunks/content-query-B4cHfrig.js.map +1 -0
  6. package/dist/chunks/content-reviews-BWrYEQc6.js +124 -0
  7. package/dist/chunks/content-reviews-BWrYEQc6.js.map +1 -0
  8. package/dist/chunks/content-versions-OEYQVrUf.js +284 -0
  9. package/dist/chunks/content-versions-OEYQVrUf.js.map +1 -0
  10. package/dist/chunks/contents-BrGxWH3s.js +612 -0
  11. package/dist/chunks/contents-BrGxWH3s.js.map +1 -0
  12. package/dist/chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  13. package/dist/content-chat-handlers.d.ts +1 -1
  14. package/dist/content-chat-handlers.d.ts.map +1 -1
  15. package/dist/content-governance.d.ts +2 -0
  16. package/dist/content-governance.d.ts.map +1 -1
  17. package/dist/content.d.ts +2 -2
  18. package/dist/content.d.ts.map +1 -1
  19. package/dist/index.js +142 -4717
  20. package/dist/index.js.map +1 -1
  21. package/dist/manifest.json +142 -5
  22. package/dist/server/content-list-actions.d.ts +123 -0
  23. package/dist/server/content-list-actions.d.ts.map +1 -0
  24. package/dist/server.d.ts +6 -0
  25. package/dist/server.d.ts.map +1 -0
  26. package/dist/server.js +874 -0
  27. package/dist/server.js.map +1 -0
  28. package/dist/smrt-knowledge.json +96 -9
  29. package/dist/svelte/components/ContentAgentChat.svelte +10 -0
  30. package/dist/svelte/components/ContentAgentChat.svelte.d.ts +10 -0
  31. package/dist/svelte/components/ContentAgentChat.svelte.d.ts.map +1 -1
  32. package/dist/svelte/components/ContentBodyEditor.svelte +9 -0
  33. package/dist/svelte/components/ContentBodyEditor.svelte.d.ts +9 -0
  34. package/dist/svelte/components/ContentBodyEditor.svelte.d.ts.map +1 -1
  35. package/dist/svelte/components/ContentBodyRenderer.svelte +3 -0
  36. package/dist/svelte/components/ContentBodyRenderer.svelte.d.ts +3 -0
  37. package/dist/svelte/components/ContentBodyRenderer.svelte.d.ts.map +1 -1
  38. package/dist/svelte/components/ContentClaimAuditTool.svelte +3 -0
  39. package/dist/svelte/components/ContentClaimAuditTool.svelte.d.ts +3 -0
  40. package/dist/svelte/components/ContentClaimAuditTool.svelte.d.ts.map +1 -1
  41. package/dist/svelte/components/ContentContributionForm.svelte +7 -0
  42. package/dist/svelte/components/ContentContributionForm.svelte.d.ts +7 -0
  43. package/dist/svelte/components/ContentContributionForm.svelte.d.ts.map +1 -1
  44. package/dist/svelte/components/ContentContributionInbox.svelte +8 -0
  45. package/dist/svelte/components/ContentContributionInbox.svelte.d.ts +8 -0
  46. package/dist/svelte/components/ContentContributionInbox.svelte.d.ts.map +1 -1
  47. package/dist/svelte/components/ContentContributionPortal.svelte +5 -0
  48. package/dist/svelte/components/ContentContributionPortal.svelte.d.ts +5 -0
  49. package/dist/svelte/components/ContentContributionPortal.svelte.d.ts.map +1 -1
  50. package/dist/svelte/components/ContentContributionTypeManager.svelte +3 -0
  51. package/dist/svelte/components/ContentContributionTypeManager.svelte.d.ts +3 -0
  52. package/dist/svelte/components/ContentContributionTypeManager.svelte.d.ts.map +1 -1
  53. package/dist/svelte/components/ContentContributorManager.svelte +3 -0
  54. package/dist/svelte/components/ContentContributorManager.svelte.d.ts +3 -0
  55. package/dist/svelte/components/ContentContributorManager.svelte.d.ts.map +1 -1
  56. package/dist/svelte/components/ContentCorrectionsTool.svelte +4 -0
  57. package/dist/svelte/components/ContentCorrectionsTool.svelte.d.ts +4 -0
  58. package/dist/svelte/components/ContentCorrectionsTool.svelte.d.ts.map +1 -1
  59. package/dist/svelte/components/ContentEditor.svelte +16 -0
  60. package/dist/svelte/components/ContentEditor.svelte.d.ts +16 -0
  61. package/dist/svelte/components/ContentEditor.svelte.d.ts.map +1 -1
  62. package/dist/svelte/components/ContentGovernanceAssignmentEditor.svelte +4 -0
  63. package/dist/svelte/components/ContentGovernanceAssignmentEditor.svelte.d.ts +4 -0
  64. package/dist/svelte/components/ContentGovernanceAssignmentEditor.svelte.d.ts.map +1 -1
  65. package/dist/svelte/components/ContentGovernanceManager.svelte +3 -0
  66. package/dist/svelte/components/ContentGovernanceManager.svelte.d.ts +3 -0
  67. package/dist/svelte/components/ContentGovernanceManager.svelte.d.ts.map +1 -1
  68. package/dist/svelte/components/ContentGovernancePanel.svelte +16 -0
  69. package/dist/svelte/components/ContentGovernancePanel.svelte.d.ts +16 -0
  70. package/dist/svelte/components/ContentGovernancePanel.svelte.d.ts.map +1 -1
  71. package/dist/svelte/components/ContentGovernancePolicyEditor.svelte +3 -0
  72. package/dist/svelte/components/ContentGovernancePolicyEditor.svelte.d.ts +3 -0
  73. package/dist/svelte/components/ContentGovernancePolicyEditor.svelte.d.ts.map +1 -1
  74. package/dist/svelte/components/ContentGovernanceProfileEditor.svelte +4 -0
  75. package/dist/svelte/components/ContentGovernanceProfileEditor.svelte.d.ts +4 -0
  76. package/dist/svelte/components/ContentGovernanceProfileEditor.svelte.d.ts.map +1 -1
  77. package/dist/svelte/components/ContentGovernanceTool.svelte +5 -0
  78. package/dist/svelte/components/ContentGovernanceTool.svelte.d.ts +5 -0
  79. package/dist/svelte/components/ContentGovernanceTool.svelte.d.ts.map +1 -1
  80. package/dist/svelte/components/ContentImageBrowser.svelte +10 -0
  81. package/dist/svelte/components/ContentImageBrowser.svelte.d.ts +10 -0
  82. package/dist/svelte/components/ContentImageBrowser.svelte.d.ts.map +1 -1
  83. package/dist/svelte/components/ContentImageChooser.svelte +4 -0
  84. package/dist/svelte/components/ContentImageChooser.svelte.d.ts +4 -0
  85. package/dist/svelte/components/ContentImageChooser.svelte.d.ts.map +1 -1
  86. package/dist/svelte/components/ContentList.svelte +1036 -24
  87. package/dist/svelte/components/ContentList.svelte.d.ts +11 -0
  88. package/dist/svelte/components/ContentList.svelte.d.ts.map +1 -1
  89. package/dist/svelte/components/ContentMetadataFields.svelte +2 -0
  90. package/dist/svelte/components/ContentMetadataFields.svelte.d.ts +2 -0
  91. package/dist/svelte/components/ContentMetadataFields.svelte.d.ts.map +1 -1
  92. package/dist/svelte/components/ContentReferencesPanel.svelte +3 -0
  93. package/dist/svelte/components/ContentReferencesPanel.svelte.d.ts +3 -0
  94. package/dist/svelte/components/ContentReferencesPanel.svelte.d.ts.map +1 -1
  95. package/dist/svelte/components/ContentReviewStatusTray.svelte +5 -0
  96. package/dist/svelte/components/ContentReviewStatusTray.svelte.d.ts +5 -0
  97. package/dist/svelte/components/ContentReviewStatusTray.svelte.d.ts.map +1 -1
  98. package/dist/svelte/components/ContentStatusFields.svelte +2 -0
  99. package/dist/svelte/components/ContentStatusFields.svelte.d.ts +2 -0
  100. package/dist/svelte/components/ContentStatusFields.svelte.d.ts.map +1 -1
  101. package/dist/svelte/components/ContentTitleField.svelte +4 -0
  102. package/dist/svelte/components/ContentTitleField.svelte.d.ts +4 -0
  103. package/dist/svelte/components/ContentTitleField.svelte.d.ts.map +1 -1
  104. package/dist/svelte/components/ContentTransparencyReport.svelte +3 -0
  105. package/dist/svelte/components/ContentTransparencyReport.svelte.d.ts +3 -0
  106. package/dist/svelte/components/ContentTransparencyReport.svelte.d.ts.map +1 -1
  107. package/dist/svelte/components/ContentTransparencyTool.svelte +3 -0
  108. package/dist/svelte/components/ContentTransparencyTool.svelte.d.ts +3 -0
  109. package/dist/svelte/components/ContentTransparencyTool.svelte.d.ts.map +1 -1
  110. package/dist/svelte/components/ContentVersionsTool.svelte +3 -0
  111. package/dist/svelte/components/ContentVersionsTool.svelte.d.ts +3 -0
  112. package/dist/svelte/components/ContentVersionsTool.svelte.d.ts.map +1 -1
  113. package/dist/svelte/components/GovernedContentEditor.svelte +20 -0
  114. package/dist/svelte/components/GovernedContentEditor.svelte.d.ts +20 -0
  115. package/dist/svelte/components/GovernedContentEditor.svelte.d.ts.map +1 -1
  116. package/dist/svelte/content-list-controller.d.ts +3 -1
  117. package/dist/svelte/content-list-controller.d.ts.map +1 -1
  118. package/dist/svelte/content-list-controller.js +15 -1
  119. package/dist/svelte/content-list-workflows.d.ts +87 -0
  120. package/dist/svelte/content-list-workflows.d.ts.map +1 -0
  121. package/dist/svelte/content-list-workflows.js +199 -0
  122. package/dist/svelte/i18n.contribution.d.ts +109 -0
  123. package/dist/svelte/i18n.contribution.d.ts.map +1 -1
  124. package/dist/svelte/i18n.contribution.js +109 -0
  125. package/dist/svelte/index.d.ts +1 -0
  126. package/dist/svelte/index.d.ts.map +1 -1
  127. package/dist/svelte/index.js +2 -0
  128. package/dist/svelte/routes/ContentContributionsRoute.svelte +4 -0
  129. package/dist/svelte/routes/ContentContributionsRoute.svelte.d.ts +4 -0
  130. package/dist/svelte/routes/ContentContributionsRoute.svelte.d.ts.map +1 -1
  131. package/dist/svelte/routes/ContentFactsRoute.svelte +5 -0
  132. package/dist/svelte/routes/ContentFactsRoute.svelte.d.ts +5 -0
  133. package/dist/svelte/routes/ContentFactsRoute.svelte.d.ts.map +1 -1
  134. package/dist/svelte/routes/ContentGovernanceRoute.svelte +4 -0
  135. package/dist/svelte/routes/ContentGovernanceRoute.svelte.d.ts +4 -0
  136. package/dist/svelte/routes/ContentGovernanceRoute.svelte.d.ts.map +1 -1
  137. package/dist/svelte/routes/ContentWorkspaceRoute.svelte +5 -0
  138. package/dist/svelte/routes/ContentWorkspaceRoute.svelte.d.ts +5 -0
  139. package/dist/svelte/routes/ContentWorkspaceRoute.svelte.d.ts.map +1 -1
  140. package/dist/svelte/routes/PublishedArticleRoute.svelte +3 -0
  141. package/dist/svelte/routes/PublishedArticleRoute.svelte.d.ts +3 -0
  142. package/dist/svelte/routes/PublishedArticleRoute.svelte.d.ts.map +1 -1
  143. package/package.json +20 -14
  144. package/dist/chunks/content-transparency-Bh6axaZj.js +0 -606
  145. package/dist/chunks/content-transparency-Bh6axaZj.js.map +0 -1
@@ -0,0 +1,612 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { D as serializeFact, E as serializeContent, J as getEffectiveContentGovernanceConfig, Q as loadPersistedContentGovernanceDefinitions, nt as resolveEffectiveContentGovernance, w as Content, x as executeContentQuery } from "./content-query-B4cHfrig.js";
3
+ import { htmlToMarkdown, resolveBodyFormat } from "../body-format.js";
4
+ import { SmrtCollection, smrt } from "@happyvertical/smrt-core";
5
+ import { queryGlobal, queryWithGlobals } from "@happyvertical/smrt-tenancy";
6
+ import { writeFile } from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { fetchDocument } from "@happyvertical/documents";
9
+ import { ensureDirectoryExists } from "@happyvertical/files";
10
+ import { createLogger } from "@happyvertical/logger";
11
+ import { makeSlug } from "@happyvertical/utils";
12
+ import YAML from "yaml";
13
+ import { lookup } from "node:dns/promises";
14
+ import { isIP } from "node:net";
15
+ //#region src/safe-remote-url.ts
16
+ async function defaultResolveHostname(hostname) {
17
+ return lookup(hostname, {
18
+ all: true,
19
+ verbatim: false
20
+ });
21
+ }
22
+ function isBlockedIPv4(address) {
23
+ const parts = address.split(".");
24
+ if (parts.length !== 4) return true;
25
+ const octets = parts.map((part) => /^\d{1,3}$/.test(part) ? Number(part) : NaN);
26
+ if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return true;
27
+ const [first, second] = octets;
28
+ return first === 0 || first === 10 || first === 127 || first >= 224 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 198 && (second === 18 || second === 19);
29
+ }
30
+ function expandIPv6(address) {
31
+ let work = address.toLowerCase().replace(/^\[|\]$/g, "");
32
+ if (work.includes(".")) {
33
+ const m = work.match(/(\d{1,3}(?:\.\d{1,3}){3})$/);
34
+ if (!m) return null;
35
+ const o = m[1].split(".").map(Number);
36
+ if (o.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
37
+ const hi = (o[0] << 8 | o[1]).toString(16);
38
+ const lo = (o[2] << 8 | o[3]).toString(16);
39
+ work = `${work.slice(0, work.length - m[1].length)}${hi}:${lo}`;
40
+ }
41
+ const halves = work.split("::");
42
+ if (halves.length > 2) return null;
43
+ const head = halves[0] ? halves[0].split(":") : [];
44
+ const tail = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
45
+ const groups = halves.length === 2 ? [
46
+ ...head,
47
+ ...Array(Math.max(0, 8 - head.length - tail.length)).fill("0"),
48
+ ...tail
49
+ ] : head;
50
+ if (groups.length !== 8) return null;
51
+ const hextets = groups.map((g) => /^[0-9a-f]{1,4}$/.test(g) ? Number.parseInt(g, 16) : NaN);
52
+ return hextets.some((n) => Number.isNaN(n)) ? null : hextets;
53
+ }
54
+ function isBlockedIPv6(address) {
55
+ const hextets = expandIPv6(address);
56
+ if (hextets) {
57
+ if (hextets.slice(0, 5).every((h) => h === 0) && (hextets[5] === 65535 || hextets[5] === 0)) {
58
+ const [, , , , , , g6, g7] = hextets;
59
+ return isBlockedIPv4(`${g6 >> 8}.${g6 & 255}.${g7 >> 8}.${g7 & 255}`);
60
+ }
61
+ }
62
+ const normalized = address.toLowerCase().replace(/^\[|\]$/g, "");
63
+ return normalized === "::" || normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || /^fe[89ab]/.test(normalized) || normalized.startsWith("ff");
64
+ }
65
+ function isBlockedAddress(address) {
66
+ const family = isIP(address);
67
+ if (family === 4) return isBlockedIPv4(address);
68
+ if (family === 6) return isBlockedIPv6(address);
69
+ return true;
70
+ }
71
+ async function assertSafeRemoteUrl(rawUrl, options = {}) {
72
+ let url;
73
+ try {
74
+ url = new URL(rawUrl);
75
+ } catch {
76
+ throw new Error("Remote URL must be an absolute URL");
77
+ }
78
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("Remote URL must use http or https");
79
+ if (url.username || url.password) throw new Error("Remote URL must not include credentials");
80
+ if (!url.hostname) throw new Error("Remote URL must include a hostname");
81
+ if (options.allowPrivateNetworkHosts) return url;
82
+ const resolver = options.resolveHostname ?? defaultResolveHostname;
83
+ const addresses = isIP(url.hostname) === 0 ? await resolver(url.hostname) : [{ address: url.hostname }];
84
+ if (!addresses.length || addresses.some(({ address }) => isBlockedAddress(address))) throw new Error("Remote URL must resolve to a public network address");
85
+ return url;
86
+ }
87
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([
88
+ 301,
89
+ 302,
90
+ 303,
91
+ 307,
92
+ 308
93
+ ]);
94
+ var DEFAULT_MAX_REDIRECTS = 5;
95
+ var DEFAULT_RESOLVE_TIMEOUT_MS = 1e4;
96
+ async function resolveSafeFinalUrl(rawUrl, options = {}) {
97
+ const fetchImpl = options.fetchImpl ?? fetch;
98
+ const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
99
+ const timeoutMs = options.timeoutMs ?? DEFAULT_RESOLVE_TIMEOUT_MS;
100
+ let current = await assertSafeRemoteUrl(rawUrl, options);
101
+ for (let hop = 0; hop <= maxRedirects; hop += 1) {
102
+ const response = await fetchImpl(current, {
103
+ method: "GET",
104
+ redirect: "manual",
105
+ signal: AbortSignal.timeout(timeoutMs)
106
+ });
107
+ try {
108
+ await response.body?.cancel();
109
+ } catch {}
110
+ if (!REDIRECT_STATUSES.has(response.status)) return current;
111
+ const location = response.headers.get("location");
112
+ if (!location) return current;
113
+ current = await assertSafeRemoteUrl(new URL(location, current).toString(), options);
114
+ }
115
+ throw new Error("Remote URL exceeded the maximum number of redirects");
116
+ }
117
+ function redactUrlCredentials(raw) {
118
+ try {
119
+ const url = new URL(raw);
120
+ if (url.username || url.password) {
121
+ url.username = "";
122
+ url.password = "";
123
+ return url.toString();
124
+ }
125
+ return raw;
126
+ } catch {
127
+ return "[unparseable url]";
128
+ }
129
+ }
130
+ //#endregion
131
+ //#region src/contents.ts
132
+ var contents_exports = /* @__PURE__ */ __exportAll({ Contents: () => Contents });
133
+ var __defProp = Object.defineProperty;
134
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
135
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
136
+ enumerable: true,
137
+ configurable: true,
138
+ writable: true,
139
+ value
140
+ }) : obj[key] = value;
141
+ var __decorateClass = (decorators, target, key, kind) => {
142
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
143
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
144
+ if (kind && result) __defProp(target, key, result);
145
+ return result;
146
+ };
147
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
148
+ var logger = createLogger({ level: "info" });
149
+ function isAIClientOptions(ai) {
150
+ return !!ai && typeof ai === "object" && !("embed" in ai) && !("generateImage" in ai);
151
+ }
152
+ var Contents = class extends SmrtCollection {
153
+ /**
154
+ * Configuration options
155
+ */
156
+ options = {};
157
+ /**
158
+ * Directory to store content files
159
+ */
160
+ contentDir;
161
+ /**
162
+ * Cache for loaded content
163
+ */
164
+ loaded;
165
+ /**
166
+ * Creates a new Contents collection
167
+ *
168
+ * Use the static `create()` method inherited from SmrtCollection for proper initialization.
169
+ *
170
+ * @param options - Configuration options
171
+ */
172
+ constructor(options = {}) {
173
+ super(options);
174
+ this.options = options;
175
+ this.loaded = /* @__PURE__ */ new Map();
176
+ }
177
+ /**
178
+ * Gets the database interface
179
+ *
180
+ * @returns Database interface
181
+ */
182
+ getDb() {
183
+ return this._db;
184
+ }
185
+ /**
186
+ * Initializes the collection
187
+ *
188
+ * @returns Promise that resolves to this instance
189
+ */
190
+ async initialize() {
191
+ await super.initialize();
192
+ return this;
193
+ }
194
+ async getFactCollection() {
195
+ const { FactCollection } = await import("@happyvertical/smrt-facts");
196
+ return FactCollection.create(this.options);
197
+ }
198
+ /**
199
+ * Bounded, tenant-safe content query (`POST /api/v1/contents/query`).
200
+ *
201
+ * Accepts the canonical `DataQueryRequest` envelope (#2444) and returns a
202
+ * normalized `DataQueryResult`, so a list surface can filter, sort, page,
203
+ * count, and facet server-side instead of hydrating the whole collection.
204
+ *
205
+ * The request body carries no authority: only schema-declared field ids are
206
+ * accepted, and tenant scoping is applied by `executeContentQuery` itself
207
+ * (fail-closed to global rows when tenancy is enabled with no active tenant
208
+ * context). Applications that need additional site/organization scoping call
209
+ * `executeContentQuery` directly with a trusted `scope`.
210
+ *
211
+ * The parameter is named `options` deliberately: the route generator passes
212
+ * the raw request body straight through as a single `options` argument, so
213
+ * the wire body IS the `DataQueryRequest` rather than a wrapper object.
214
+ *
215
+ * @param options Untrusted `DataQueryRequest` from the caller.
216
+ * @returns A validated, bounded `DataQueryResult`.
217
+ */
218
+ async queryAction(options) {
219
+ return executeContentQuery(this, options);
220
+ }
221
+ async browseFacts(options = {}) {
222
+ try {
223
+ const facts = await this.getFactCollection();
224
+ const query = options.query || options.q || "";
225
+ const limit = options.limit !== void 0 ? Number(options.limit) : void 0;
226
+ const offset = options.offset !== void 0 ? Number(options.offset) : void 0;
227
+ const minSimilarity = options.minSimilarity !== void 0 ? Number(options.minSimilarity) : void 0;
228
+ const includeSuperseded = options.includeSuperseded === true || options.includeSuperseded === "true";
229
+ const latestOnly = options.latestOnly === void 0 ? true : options.latestOnly === true || options.latestOnly === "true";
230
+ return (await facts.browseCatalog(query, {
231
+ limit: Number.isFinite(limit) ? limit : void 0,
232
+ offset: Number.isFinite(offset) ? offset : void 0,
233
+ minSimilarity: Number.isFinite(minSimilarity) ? minSimilarity : void 0,
234
+ includeSuperseded,
235
+ latestOnly,
236
+ tenantId: options.tenantId ?? null
237
+ })).map(serializeFact);
238
+ } catch (error) {
239
+ if (typeof error === "object" && error !== null && error.code === "DB_SCHEMA_MISSING") return [];
240
+ throw error;
241
+ }
242
+ }
243
+ async getBySlug(options = {}) {
244
+ if (!options.slug) throw new Error("slug is required");
245
+ const where = {
246
+ slug: options.slug,
247
+ context: options.context || ""
248
+ };
249
+ if (options.tenantId !== void 0) where.tenantId = options.tenantId;
250
+ const content = await this.get(where);
251
+ if (!content) return null;
252
+ if (options.status && content.status !== options.status) return null;
253
+ return serializeContent(content);
254
+ }
255
+ async getGovernanceDefinitionsAction(options = {}) {
256
+ const [effective, persisted] = await Promise.all([getEffectiveContentGovernanceConfig({
257
+ db: this.db,
258
+ tenantId: options.tenantId
259
+ }), loadPersistedContentGovernanceDefinitions({
260
+ db: this.db,
261
+ tenantId: options.tenantId
262
+ })]);
263
+ return {
264
+ effective: {
265
+ policies: effective.policies.map((policy) => ({
266
+ ...policy,
267
+ ...persisted.policies.find((item) => item.key === policy.key) || {}
268
+ })),
269
+ profiles: effective.profiles.map((profile) => ({
270
+ ...profile,
271
+ ...persisted.profiles.find((item) => item.key === profile.key) || {}
272
+ })),
273
+ assignments: effective.assignments.map((assignment) => ({
274
+ ...assignment,
275
+ ...persisted.assignments.find((item) => item.key === assignment.key) || {}
276
+ }))
277
+ },
278
+ persisted: {
279
+ policies: persisted.policies,
280
+ profiles: persisted.profiles,
281
+ assignments: persisted.assignments
282
+ }
283
+ };
284
+ }
285
+ async resolveGovernanceAction(options = {}) {
286
+ return resolveEffectiveContentGovernance({
287
+ contentType: options.type || null,
288
+ contentVariant: options.variant || null,
289
+ db: this.db,
290
+ tenantId: options.tenantId
291
+ });
292
+ }
293
+ /**
294
+ * Mirrors content from a remote URL
295
+ *
296
+ * Downloads and stores content from a remote URL, extracting text
297
+ * and saving it as a Content object.
298
+ *
299
+ * @param options - Mirror options
300
+ * @param options.url - URL to mirror
301
+ * @param options.mirrorDir - Directory for caching mirrored files
302
+ * @param options.context - Context for the mirrored content
303
+ * @returns Promise resolving to the mirrored Content object
304
+ * @throws Error if URL is invalid or missing
305
+ */
306
+ async mirror(options) {
307
+ if (!options.url) throw new Error("No URL provided");
308
+ let url;
309
+ try {
310
+ url = await resolveSafeFinalUrl(options.url, {
311
+ allowPrivateNetworkHosts: options.allowPrivateNetworkHosts,
312
+ resolveHostname: options.resolveHostname,
313
+ fetchImpl: options.fetchImpl
314
+ });
315
+ } catch (error) {
316
+ const safeUrl = redactUrlCredentials(options.url);
317
+ logger.error("Refusing to mirror unsafe URL", {
318
+ error,
319
+ url: safeUrl
320
+ });
321
+ throw new Error(`Invalid URL provided: ${safeUrl}`);
322
+ }
323
+ const existing = await this.get({ url: options.url });
324
+ if (existing) return existing;
325
+ const doc = await fetchDocument(url.toString(), { cacheDir: options?.mirrorDir });
326
+ const title = (url.pathname.split("/").pop()?.replace(/\.[^/.]+$/, ""))?.replace(/[-_]/g, " ");
327
+ const slug = makeSlug(title);
328
+ const body = doc.parts.map((part) => part.content).join("\n\n");
329
+ if (body) {
330
+ const content = new Content({
331
+ url: options.url,
332
+ type: "mirror",
333
+ title,
334
+ slug,
335
+ context: options.context || "",
336
+ body
337
+ });
338
+ await content.initialize();
339
+ await content.save();
340
+ return content;
341
+ }
342
+ }
343
+ /**
344
+ * Writes a Content object to the filesystem as a markdown file
345
+ *
346
+ * @param options - Options for writing the content file
347
+ * @param options.content - Content object to write
348
+ * @param options.contentDir - Directory to write the file to
349
+ * @returns Promise that resolves when the file is written
350
+ * @throws Error if contentDir is not provided
351
+ */
352
+ async writeContentFile(options) {
353
+ const { content, contentDir } = options;
354
+ if (!contentDir) throw new Error("No content dir provided");
355
+ const { body } = content;
356
+ const frontMatter = {
357
+ title: content.title,
358
+ slug: content.slug,
359
+ context: content.context,
360
+ author: content.author,
361
+ publish_date: content.publish_date
362
+ };
363
+ let output = "";
364
+ if (frontMatter && Object.keys(frontMatter).length > 0) {
365
+ output += "---\n";
366
+ output += YAML.stringify(frontMatter);
367
+ output += "---\n";
368
+ }
369
+ let formattedBody = body || "";
370
+ if (resolveBodyFormat(content.bodyFormat, body) === "html") formattedBody = htmlToMarkdown(body || "");
371
+ else if (body && !this.isMarkdown(body)) formattedBody = this.formatAsMarkdown(body);
372
+ output += formattedBody;
373
+ const pathParts = [
374
+ contentDir,
375
+ content.context || "",
376
+ content.slug,
377
+ "index.md"
378
+ ].filter(Boolean);
379
+ const outputFile = path.join(...pathParts);
380
+ const resolvedDir = path.resolve(contentDir);
381
+ const resolvedFile = path.resolve(outputFile);
382
+ if (resolvedFile !== resolvedDir && !resolvedFile.startsWith(resolvedDir + path.sep)) throw new Error("Refusing to write content file outside of the content directory");
383
+ await ensureDirectoryExists(path.dirname(outputFile));
384
+ await writeFile(outputFile, output);
385
+ }
386
+ /**
387
+ * Checks if text appears to be in markdown format
388
+ *
389
+ * @param text - Text to check
390
+ * @returns Boolean indicating if the text contains markdown syntax
391
+ */
392
+ isMarkdown(text) {
393
+ return [
394
+ /^#\s/m,
395
+ /\*\*.+\*\*/,
396
+ /\*.+\*/,
397
+ /\[.+\]\(.+\)/,
398
+ /^\s*[-*+]\s/m,
399
+ /^\s*\d+\.\s/m,
400
+ /```[\s\S]*```/,
401
+ /^\s*>/m
402
+ ].some((indicator) => indicator.test(text));
403
+ }
404
+ /**
405
+ * Formats plain text as simple markdown
406
+ *
407
+ * @param text - Plain text to format
408
+ * @returns Text formatted as basic markdown
409
+ */
410
+ formatAsMarkdown(text) {
411
+ return text.split(/\n\n+/).map((paragraph) => paragraph.trim()).filter(Boolean).join("\n\n");
412
+ }
413
+ /**
414
+ * Synchronizes content to the filesystem
415
+ *
416
+ * Writes all article-type Content objects to the filesystem
417
+ * as markdown files.
418
+ *
419
+ * @param options - Sync options
420
+ * @param options.contentDir - Directory to write content files to
421
+ * @returns Promise that resolves when synchronization is complete
422
+ */
423
+ async syncContentDir(options) {
424
+ const contents = await this.list({ where: { type: "article" } });
425
+ for (const content of contents) await this.writeContentFile({
426
+ content,
427
+ contentDir: options.contentDir || this.options.contentDir || ""
428
+ });
429
+ }
430
+ /**
431
+ * Generate thumbnails for content that doesn't have one
432
+ *
433
+ * @param options - Options for bulk thumbnail generation
434
+ * @returns Promise resolving to result object with generated images and failed content IDs
435
+ *
436
+ * @example Generate headline cards for all published articles
437
+ * ```typescript
438
+ * const result = await contents.generateMissingThumbnails({
439
+ * strategy: 'headline-card',
440
+ * where: { type: 'article', status: 'published' },
441
+ * brandColor: '#1a56db'
442
+ * });
443
+ * console.log(`Generated ${result.images.length} thumbnails`);
444
+ * if (result.failed.length > 0) {
445
+ * console.warn(`Failed to generate ${result.failed.length} thumbnails`);
446
+ * }
447
+ * ```
448
+ */
449
+ async generateMissingThumbnails(options) {
450
+ const mergedOptions = {
451
+ ...this.options?.thumbnail || {},
452
+ ...options
453
+ };
454
+ const whereClause = {
455
+ ...mergedOptions.where,
456
+ thumbnailAssetId: null
457
+ };
458
+ const contents = await this.list({
459
+ where: whereClause,
460
+ limit: mergedOptions.limit
461
+ });
462
+ const generatedImages = [];
463
+ const failed = [];
464
+ for (const content of contents) try {
465
+ let thumbnailOptions;
466
+ switch (mergedOptions.strategy) {
467
+ case "headline-card":
468
+ thumbnailOptions = {
469
+ strategy: "headline-card",
470
+ brandColor: mergedOptions.brandColor,
471
+ backgroundColor: mergedOptions.backgroundColor,
472
+ logoUrl: mergedOptions.logoUrl,
473
+ template: mergedOptions.template,
474
+ width: mergedOptions.width,
475
+ height: mergedOptions.height
476
+ };
477
+ break;
478
+ case "static-map":
479
+ thumbnailOptions = {
480
+ strategy: "static-map",
481
+ mapProvider: mergedOptions.mapProvider,
482
+ zoom: mergedOptions.zoom,
483
+ width: mergedOptions.width,
484
+ height: mergedOptions.height
485
+ };
486
+ break;
487
+ case "ai-generate":
488
+ thumbnailOptions = {
489
+ strategy: "ai-generate",
490
+ style: mergedOptions.style,
491
+ width: mergedOptions.width,
492
+ height: mergedOptions.height,
493
+ ai: isAIClientOptions(this.options.ai) ? this.options.ai : void 0
494
+ };
495
+ break;
496
+ default: throw new Error(`Unknown strategy: ${mergedOptions.strategy}`);
497
+ }
498
+ const image = await content.generateThumbnail(thumbnailOptions);
499
+ generatedImages.push(image);
500
+ } catch (error) {
501
+ const errorMessage = error instanceof Error ? error.message : String(error);
502
+ logger.error(`Failed to generate thumbnail for content ${content.id}: ${errorMessage}`);
503
+ failed.push({
504
+ contentId: content.id ?? "unknown",
505
+ error: errorMessage
506
+ });
507
+ }
508
+ return {
509
+ images: generatedImages,
510
+ failed
511
+ };
512
+ }
513
+ /**
514
+ * Find all content belonging to a specific tenant
515
+ *
516
+ * @param tenantId - The tenant ID to filter by
517
+ * @returns Promise resolving to array of Content objects for the tenant
518
+ *
519
+ * @example
520
+ * ```typescript
521
+ * const tenantContent = await contents.findByTenant('tenant-123');
522
+ * ```
523
+ */
524
+ async findByTenant(tenantId) {
525
+ return this.list({ where: { tenantId } });
526
+ }
527
+ /**
528
+ * Find all global content (not associated with any tenant).
529
+ *
530
+ * Routes through the shared tenant-global helper so it does not throw under
531
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
532
+ * flagged as an isolation violation). (#1600)
533
+ *
534
+ * @returns Promise resolving to array of global Content objects
535
+ *
536
+ * @example
537
+ * ```typescript
538
+ * const globalContent = await contents.findGlobal();
539
+ * ```
540
+ */
541
+ async findGlobal() {
542
+ return queryGlobal(this);
543
+ }
544
+ /**
545
+ * Find content for a tenant including global content.
546
+ *
547
+ * This returns both tenant-specific content and global content (tenantId is null),
548
+ * useful for showing a tenant their content plus any shared/global resources.
549
+ *
550
+ * Fails closed if an active tenant context requests a different tenant's
551
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
552
+ *
553
+ * @param tenantId - The tenant ID to include
554
+ * @returns Promise resolving to array of Content objects (tenant + global)
555
+ *
556
+ * @example
557
+ * ```typescript
558
+ * const allAccessibleContent = await contents.findWithGlobals('tenant-123');
559
+ * ```
560
+ */
561
+ async findWithGlobals(tenantId) {
562
+ return queryWithGlobals(this, tenantId, "Content.findWithGlobals");
563
+ }
564
+ };
565
+ /**
566
+ * Class constructor for collection items
567
+ */
568
+ __publicField(Contents, "_itemClass", Content);
569
+ Contents = __decorateClass([smrt({
570
+ api: {
571
+ include: [
572
+ "browseFacts",
573
+ "getBySlug",
574
+ "getGovernanceDefinitionsAction",
575
+ "resolveGovernanceAction",
576
+ "queryAction"
577
+ ],
578
+ routes: {
579
+ queryAction: {
580
+ scope: "collection",
581
+ method: "POST",
582
+ path: "query"
583
+ },
584
+ browseFacts: {
585
+ scope: "collection",
586
+ method: "GET",
587
+ path: "facts"
588
+ },
589
+ getBySlug: {
590
+ scope: "collection",
591
+ method: "GET",
592
+ path: "by-slug"
593
+ },
594
+ getGovernanceDefinitionsAction: {
595
+ scope: "collection",
596
+ method: "GET",
597
+ path: "governance"
598
+ },
599
+ resolveGovernanceAction: {
600
+ scope: "collection",
601
+ method: "GET",
602
+ path: "governance/resolve"
603
+ }
604
+ }
605
+ },
606
+ mcp: false,
607
+ cli: false
608
+ })], Contents);
609
+ //#endregion
610
+ export { contents_exports as n, assertSafeRemoteUrl as r, Contents as t };
611
+
612
+ //# sourceMappingURL=contents-BrGxWH3s.js.map