@happyvertical/smrt-content 0.58.0 → 0.59.1
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/dist/chunks/{content-query-mlGs7tPQ.js → content-query-DcVNRGoy.js} +4 -4
- package/dist/chunks/{content-query-mlGs7tPQ.js.map → content-query-DcVNRGoy.js.map} +1 -1
- package/dist/chunks/{content-versions-C73TZaoy.js → content-versions-BnCiRXbb.js} +2 -2
- package/dist/chunks/{content-versions-C73TZaoy.js.map → content-versions-BnCiRXbb.js.map} +1 -1
- package/dist/chunks/{contents-CdF4fURL.js → contents-DDiybAlF.js} +2 -2
- package/dist/chunks/{contents-CdF4fURL.js.map → contents-DDiybAlF.js.map} +1 -1
- package/dist/index.js +4 -4
- package/dist/manifest.json +1 -1
- package/dist/server.js +1 -1
- package/dist/smrt-knowledge.json +3 -3
- package/package.json +16 -16
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { A as normalizeContentTransparency, rt as resolveEffectiveContentGovernance } from "./content-query-
|
|
2
|
+
import { A as normalizeContentTransparency, rt as resolveEffectiveContentGovernance } from "./content-query-DcVNRGoy.js";
|
|
3
3
|
import { SmrtCollection, SmrtObject, field, foreignKey, smrt } from "@happyvertical/smrt-core";
|
|
4
4
|
import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
|
|
5
5
|
//#region src/content-version.ts
|
|
@@ -298,4 +298,4 @@ var ContentVersionCollection = class extends SmrtCollection {
|
|
|
298
298
|
//#endregion
|
|
299
299
|
export { content_versions_exports as n, ContentVersion as r, ContentVersionCollection as t };
|
|
300
300
|
|
|
301
|
-
//# sourceMappingURL=content-versions-
|
|
301
|
+
//# sourceMappingURL=content-versions-BnCiRXbb.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-versions-C73TZaoy.js","names":[],"sources":["../../src/content-version.ts","../../src/content-versions.ts"],"sourcesContent":["import type { SmrtObjectOptions } from '@happyvertical/smrt-core';\nimport { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { ContentVersionKind } from './content-governance';\nimport { normalizeContentTransparency } from './content-transparency';\n\n/**\n * Shape of the loosely-structured `metadata` JSON bag stored on a content\n * version. Known fields are typed; unrecognized keys remain accessible via the\n * index signature.\n */\nexport interface ContentVersionMetadata {\n /**\n * Fingerprint of the snapshot captured for the latest publication version,\n * used to detect post-publication drift.\n */\n publicationSnapshotFingerprint?: string;\n [key: string]: unknown;\n}\n\nexport interface ContentVersionOptions extends SmrtObjectOptions {\n contentId?: string;\n version?: number;\n kind?: ContentVersionKind;\n title?: string;\n description?: string;\n body?: string;\n status?: string;\n summary?: string;\n snapshot?: string | Record<string, unknown>;\n metadata?: string | Record<string, unknown>;\n tenantId?: string | null;\n createdAt?: Date;\n updatedAt?: Date;\n}\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'content_versions',\n conflictColumns: ['content_id', 'version'],\n api: {\n include: ['list', 'get', 'create', 'getTransparencyAction'],\n routes: {\n getTransparencyAction: { method: 'GET', path: 'transparency' },\n },\n },\n mcp: { include: ['list', 'get', 'create'] },\n cli: true,\n})\nexport class ContentVersion extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-content\";\n @foreignKey('Content', { required: true, onDelete: 'CASCADE' })\n @field({ description: 'The piece this version belongs to.' })\n contentId = '';\n\n @field({ description: 'The number of this version, counting up from one.' })\n version = 1;\n\n @field({\n description:\n 'Why the version was saved: a draft, a review, a publication or a correction.',\n })\n kind: ContentVersionKind = 'manual';\n\n @field({ description: 'The headline as it was in this version.' })\n title = '';\n description = '';\n body = '';\n\n @field({ description: 'Where the piece stood when this version was saved.' })\n status = 'draft';\n\n @field({ description: 'A short note on what changed in this version.' })\n summary = '';\n snapshot = '{}';\n metadata = '';\n\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @field({ description: 'When this version was saved.' })\n createdAt = new Date();\n updatedAt = new Date();\n\n constructor(options: ContentVersionOptions = {}) {\n super(options);\n if (options.contentId) this.contentId = options.contentId;\n if (options.version !== undefined) this.version = options.version;\n if (options.kind !== undefined) this.kind = options.kind;\n if (options.title !== undefined) this.title = options.title;\n if (options.description !== undefined)\n this.description = options.description;\n if (options.body !== undefined) this.body = options.body;\n if (options.status !== undefined) this.status = options.status;\n if (options.summary !== undefined) this.summary = options.summary;\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.createdAt) this.createdAt = options.createdAt;\n if (options.updatedAt) this.updatedAt = options.updatedAt;\n\n if (options.snapshot !== undefined) {\n this.snapshot =\n typeof options.snapshot === 'string'\n ? options.snapshot\n : JSON.stringify(options.snapshot);\n }\n\n if (options.metadata !== undefined) {\n this.metadata =\n typeof options.metadata === 'string'\n ? options.metadata\n : JSON.stringify(options.metadata);\n }\n }\n\n getSnapshot(): Record<string, unknown> {\n try {\n return this.snapshot ? JSON.parse(this.snapshot) : {};\n } catch {\n return {};\n }\n }\n\n getMetadata(): ContentVersionMetadata {\n try {\n return this.metadata ? JSON.parse(this.metadata) : {};\n } catch {\n return {};\n }\n }\n\n getTransparency() {\n const metadata = this.getMetadata();\n const snapshot = this.getSnapshot();\n\n return normalizeContentTransparency(metadata.transparency, {\n snapshotKind: this.kind === 'publication' ? 'published' : 'preview',\n contentId: this.contentId || (snapshot.contentId as string) || null,\n currentContentStatus:\n this.status || (snapshot.status as string) || 'draft',\n publicationVersion: {\n id: (this.id as string) || null,\n version: this.version ?? null,\n kind: this.kind || null,\n summary: this.summary || '',\n createdAt:\n this.createdAt instanceof Date ? this.createdAt.toISOString() : null,\n },\n });\n }\n\n async getTransparencyAction() {\n return this.getTransparency();\n }\n}\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport type { FactContentRelationship } from '@happyvertical/smrt-facts';\nimport type { Content } from './content';\nimport type { CreateContentVersionOptions } from './content-governance';\nimport { resolveEffectiveContentGovernance } from './content-governance';\nimport { ContentVersion } from './content-version';\n\nexport class ContentVersionCollection extends SmrtCollection<ContentVersion> {\n static readonly _itemClass = ContentVersion;\n\n private buildSnapshotFactRelationships(\n snapshot: Record<string, unknown>,\n defaultRelationship: FactContentRelationship,\n ): Map<FactContentRelationship, string[]> {\n const byRelationship = new Map<FactContentRelationship, string[]>();\n const rawLinks: unknown[] = Array.isArray(snapshot.factLinks)\n ? snapshot.factLinks\n : [];\n\n for (const rawLink of rawLinks) {\n const link = rawLink as {\n factId?: unknown;\n relationship?: unknown;\n } | null;\n const factId =\n typeof link?.factId === 'string' && link.factId.length > 0\n ? link.factId\n : null;\n const relationship =\n typeof link?.relationship === 'string' && link.relationship.length > 0\n ? (link.relationship as FactContentRelationship)\n : defaultRelationship;\n\n if (!factId) {\n continue;\n }\n\n byRelationship.set(relationship, [\n ...(byRelationship.get(relationship) || []),\n factId,\n ]);\n }\n\n if (\n byRelationship.size === 0 &&\n Array.isArray(snapshot.factIds) &&\n snapshot.factIds.length > 0\n ) {\n byRelationship.set(\n defaultRelationship,\n snapshot.factIds.filter(\n (factId: unknown): factId is string =>\n typeof factId === 'string' && factId.length > 0,\n ),\n );\n }\n\n return byRelationship;\n }\n\n async listForContent(contentId: string): Promise<ContentVersion[]> {\n return this.list({\n where: { contentId },\n orderBy: 'version ASC',\n });\n }\n\n async getLatestForContent(contentId: string): Promise<ContentVersion | null> {\n const versions = await this.listForContent(contentId);\n return versions.length > 0 ? versions[versions.length - 1] : null;\n }\n\n async getLatestPublishedForContent(\n contentId: string,\n ): Promise<ContentVersion | null> {\n const versions = await this.list({\n where: {\n contentId,\n kind: 'publication',\n },\n orderBy: 'version DESC',\n });\n\n return versions[0] || null;\n }\n\n async getVersion(\n contentId: string,\n versionNumber: number,\n ): Promise<ContentVersion | null> {\n return this.get({\n contentId,\n version: versionNumber,\n });\n }\n\n async getNextVersionNumber(contentId: string): Promise<number> {\n const latest = await this.getLatestForContent(contentId);\n return latest ? latest.version + 1 : 1;\n }\n\n async createSnapshot(\n content: Content,\n options: CreateContentVersionOptions = {},\n ): Promise<ContentVersion> {\n if (!content.id) {\n throw new Error('Cannot create a version for unsaved content');\n }\n\n const version = await this.getNextVersionNumber(content.id as string);\n const governance = await resolveEffectiveContentGovernance({\n contentType: content.type,\n contentVariant: content.variant,\n db: this.db,\n tenantId: content.tenantId ?? null,\n });\n const [references, referenceEdges, assets, factsState] = await Promise.all([\n typeof content.getReferences === 'function'\n ? content.getReferences()\n : [],\n // Capture per-edge citation pins so restore can reconstruct them.\n // `getReferences()` resolves to Content objects and loses targetVersion.\n typeof content.getReferenceEdges === 'function'\n ? content.getReferenceEdges()\n : Promise.resolve([]),\n typeof content.getAssets === 'function' ? content.getAssets() : [],\n typeof content.getFactsState === 'function' &&\n governance.factLinkingEnabled\n ? content.getFactsState()\n : {\n factIds: [],\n facts: [],\n factLinks: [],\n },\n ]);\n\n const baseSnapshot = {\n id: content.id,\n slug: content.slug,\n context: content.context,\n name: content.name,\n type: content.type,\n variant: content.variant,\n fileKey: content.fileKey,\n author: content.author,\n title: content.title,\n description: content.description,\n body: content.body,\n bodyFormat: content.bodyFormat,\n publish_date: content.publish_date,\n url: content.url,\n source: content.source,\n original_url: content.original_url,\n language: content.language,\n tags: [...content.tags],\n category: content.category,\n status: content.status,\n state: content.state,\n metadata: content.metadata,\n thumbnailAssetId: content.thumbnailAssetId,\n referenceIds: references.map((reference) => reference.id).filter(Boolean),\n // Full edges with citation pins; `referenceIds` retained for back-compat\n // with snapshots written before pin-aware restore (#1387 #3).\n referenceEdges: referenceEdges.filter((edge) => Boolean(edge.targetId)),\n assetIds: assets.map((asset) => asset.id).filter(Boolean),\n factIds: factsState.factIds,\n factLinks: factsState.factLinks,\n tenantId: content.tenantId,\n _meta_type: content.toJSON()._meta_type,\n };\n const snapshot = {\n ...baseSnapshot,\n ...(options.snapshot || {}),\n };\n const versionSlugBase =\n snapshot.slug ||\n content.slug ||\n content.name ||\n content.title ||\n content.id;\n const versionSlug = `${versionSlugBase}-v${version}`;\n\n return this.create({\n slug: versionSlug,\n context: content.context || '',\n contentId: content.id as string,\n version,\n kind: options.kind || 'manual',\n title: snapshot.title || '',\n description: snapshot.description || '',\n body: snapshot.body || '',\n status: snapshot.status || 'draft',\n summary: options.summary || '',\n snapshot: JSON.stringify(snapshot),\n metadata: JSON.stringify(options.metadata || {}),\n tenantId: content.tenantId,\n });\n }\n\n async restoreIntoContent(\n content: Content,\n versionNumber: number,\n ): Promise<Content> {\n if (!content.id) {\n throw new Error('Cannot restore an unsaved content item');\n }\n\n const version = await this.getVersion(content.id as string, versionNumber);\n if (!version) {\n throw new Error(\n `Content version ${versionNumber} not found for content ${content.id}`,\n );\n }\n\n const snapshot = version.getSnapshot();\n const keysToRestore = [\n 'name',\n 'type',\n 'variant',\n 'fileKey',\n 'author',\n 'title',\n 'description',\n 'body',\n 'bodyFormat',\n 'publish_date',\n 'url',\n 'source',\n 'original_url',\n 'language',\n 'tags',\n 'category',\n 'status',\n 'state',\n 'metadata',\n 'thumbnailAssetId',\n ];\n\n // Restore snapshot values onto the live Content instance by field name.\n // Indexing a class instance by an arbitrary string key requires a record\n // view; the keys are a fixed, known set of Content fields.\n const writableContent = content as unknown as Record<string, unknown>;\n for (const key of keysToRestore) {\n if (snapshot[key] !== undefined) {\n writableContent[key] = snapshot[key];\n }\n }\n\n // Reference edges with citation pins (#1387 #3). Newer snapshots carry\n // `referenceEdges` ({ targetId, targetVersion }); older ones only have\n // `referenceIds`. Either way, seed the pending `referenceIds` from the\n // target ids so `save()` reconciles the set (adds missing, removes extra),\n // then re-apply the saved pins below so restoring \"to vN\" reconstructs the\n // citation pins that existed at vN instead of dropping them to unpinned.\n const snapshotEdges: Array<{\n targetId: string;\n targetVersion: number | null;\n }> = Array.isArray(snapshot.referenceEdges)\n ? (snapshot.referenceEdges as unknown[])\n .filter(\n (edge): edge is { targetId: string; targetVersion?: unknown } =>\n !!edge &&\n typeof edge === 'object' &&\n typeof (edge as { targetId?: unknown }).targetId === 'string' &&\n (edge as { targetId: string }).targetId.length > 0,\n )\n .map((edge) => ({\n targetId: edge.targetId,\n targetVersion:\n typeof edge.targetVersion === 'number'\n ? edge.targetVersion\n : null,\n }))\n : Array.isArray(snapshot.referenceIds)\n ? snapshot.referenceIds\n .filter(\n (id: unknown): id is string =>\n typeof id === 'string' && id.length > 0,\n )\n .map((targetId: string) => ({ targetId, targetVersion: null }))\n : [];\n\n if (\n Array.isArray(snapshot.referenceEdges) ||\n Array.isArray(snapshot.referenceIds)\n ) {\n writableContent.referenceIds = snapshotEdges.map((edge) => edge.targetId);\n }\n\n if (Array.isArray(snapshot.assetIds)) {\n writableContent.assetIds = [...snapshot.assetIds];\n }\n\n await content.save();\n\n // Re-apply the citation pin of EVERY snapshot edge — including UNPINNED\n // ones (`targetVersion: null`). `save()` only reconciles the target-id set\n // (adds missing / removes extra edges) and leaves the pin of an edge that\n // already existed untouched. So restoring an *unpinned* snapshot over an\n // edge that is currently *pinned* must explicitly clear that pin, otherwise\n // the live pin survives the restore and drift never resets. Passing\n // `addReference(target, { targetVersion: null })` clears the pin in place\n // (the junction's `attach` updates the row when `null !== existing`), while\n // a non-null value (re)sets it — so \"restore to vN\" reconstructs exactly\n // the pins that existed at vN.\n //\n // We pass the resolved Content object (not the raw id) because\n // `addReference(string)` treats the string as a URL, not a content id.\n // `addReference` is idempotent on (source, target) and only adjusts\n // targetVersion.\n if (\n snapshotEdges.length > 0 &&\n typeof content.getReferences === 'function' &&\n typeof content.addReference === 'function'\n ) {\n const resolvedReferences = await content.getReferences();\n const resolvedById = new Map(\n resolvedReferences\n .filter((reference) => reference.id)\n .map((reference) => [reference.id as string, reference]),\n );\n for (const edge of snapshotEdges) {\n const target = resolvedById.get(edge.targetId);\n if (target) {\n await content.addReference(target, {\n targetVersion: edge.targetVersion,\n });\n }\n }\n }\n\n const governance = await resolveEffectiveContentGovernance({\n contentType: content.type,\n contentVariant: content.variant,\n db: this.db,\n tenantId: content.tenantId ?? null,\n });\n\n if (\n governance.isGoverned &&\n governance.factLinkingEnabled &&\n typeof content.getFactLinks === 'function' &&\n typeof content.syncFacts === 'function'\n ) {\n const desiredByRelationship = this.buildSnapshotFactRelationships(\n snapshot,\n governance.defaultFactRelationship,\n );\n const currentLinks = await content.getFactLinks();\n const currentRelationships = new Set(\n currentLinks.map(\n (link) =>\n (link.relationship as FactContentRelationship) ||\n governance.defaultFactRelationship,\n ),\n );\n const relationshipsToSync = new Set<FactContentRelationship>([\n ...currentRelationships,\n ...desiredByRelationship.keys(),\n ]);\n\n for (const relationship of relationshipsToSync) {\n await content.syncFacts(\n desiredByRelationship.get(relationship) || [],\n relationship,\n );\n }\n }\n\n return content;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAiDO,IAAM,iBAAN,cAA6B,WAAW;CAG7C,YAAY;CAGZ,UAAU;CAMV,OAA2B;CAG3B,QAAQ;CACR,cAAc;CACd,OAAO;CAGP,SAAS;CAGT,UAAU;CACV,WAAW;CACX,WAAW;CAGX,WAA0B;CAG1B,4BAAY,IAAI,KAAK;CACrB,4BAAY,IAAI,KAAK;CAErB,YAAY,UAAiC,CAAC,GAAG;EAC/C,MAAM,OAAO;EACb,IAAI,QAAQ,WAAW,KAAK,YAAY,QAAQ;EAChD,IAAI,QAAQ,YAAY,KAAA,GAAW,KAAK,UAAU,QAAQ;EAC1D,IAAI,QAAQ,SAAS,KAAA,GAAW,KAAK,OAAO,QAAQ;EACpD,IAAI,QAAQ,UAAU,KAAA,GAAW,KAAK,QAAQ,QAAQ;EACtD,IAAI,QAAQ,gBAAgB,KAAA,GAC1B,KAAK,cAAc,QAAQ;EAC7B,IAAI,QAAQ,SAAS,KAAA,GAAW,KAAK,OAAO,QAAQ;EACpD,IAAI,QAAQ,WAAW,KAAA,GAAW,KAAK,SAAS,QAAQ;EACxD,IAAI,QAAQ,YAAY,KAAA,GAAW,KAAK,UAAU,QAAQ;EAC1D,IAAI,QAAQ,aAAa,KAAA,GAAW,KAAK,WAAW,QAAQ;EAC5D,IAAI,QAAQ,WAAW,KAAK,YAAY,QAAQ;EAChD,IAAI,QAAQ,WAAW,KAAK,YAAY,QAAQ;EAEhD,IAAI,QAAQ,aAAa,KAAA,GACvB,KAAK,WACH,OAAO,QAAQ,aAAa,WACxB,QAAQ,WACR,KAAK,UAAU,QAAQ,QAAQ;EAGvC,IAAI,QAAQ,aAAa,KAAA,GACvB,KAAK,WACH,OAAO,QAAQ,aAAa,WACxB,QAAQ,WACR,KAAK,UAAU,QAAQ,QAAQ;CAEzC;CAEA,cAAuC;EACrC,IAAI;GACF,OAAO,KAAK,WAAW,KAAK,MAAM,KAAK,QAAQ,IAAI,CAAC;EACtD,QAAQ;GACN,OAAO,CAAC;EACV;CACF;CAEA,cAAsC;EACpC,IAAI;GACF,OAAO,KAAK,WAAW,KAAK,MAAM,KAAK,QAAQ,IAAI,CAAC;EACtD,QAAQ;GACN,OAAO,CAAC;EACV;CACF;CAEA,kBAAkB;EAChB,MAAM,WAAW,KAAK,YAAY;EAClC,MAAM,WAAW,KAAK,YAAY;EAElC,OAAO,6BAA6B,SAAS,cAAc;GACzD,cAAc,KAAK,SAAS,gBAAgB,cAAc;GAC1D,WAAW,KAAK,aAAc,SAAS,aAAwB;GAC/D,sBACE,KAAK,UAAW,SAAS,UAAqB;GAChD,oBAAoB;IAClB,IAAK,KAAK,MAAiB;IAC3B,SAAS,KAAK,WAAW;IACzB,MAAM,KAAK,QAAQ;IACnB,SAAS,KAAK,WAAW;IACzB,WACE,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;GACpE;EACF,CAAC;CACH;CAEA,MAAM,wBAAwB;EAC5B,OAAO,KAAK,gBAAgB;CAC9B;AACF;AAvGiD,cAApC,gBAA2C,mBAAkB,6BAAA;AAGxE,gBAAA,CAFC,WAAW,WAAW;CAAE,UAAU;CAAM,UAAU;AAAU,CAAC,GAC7D,MAAM,EAAE,aAAa,qCAAqC,CAAC,CAAA,GAFjD,eAGX,WAAA,aAAA,CAAA;AAGA,gBAAA,CADC,MAAM,EAAE,aAAa,oDAAoD,CAAC,CAAA,GALhE,eAMX,WAAA,WAAA,CAAA;AAMA,gBAAA,CAJC,MAAM,EACL,aACE,+EACJ,CAAC,CAAA,GAXU,eAYX,WAAA,QAAA,CAAA;AAGA,gBAAA,CADC,MAAM,EAAE,aAAa,0CAA0C,CAAC,CAAA,GAdtD,eAeX,WAAA,SAAA,CAAA;AAKA,gBAAA,CADC,MAAM,EAAE,aAAa,qDAAqD,CAAC,CAAA,GAnBjE,eAoBX,WAAA,UAAA,CAAA;AAGA,gBAAA,CADC,MAAM,EAAE,aAAa,gDAAgD,CAAC,CAAA,GAtB5D,eAuBX,WAAA,WAAA,CAAA;AAKA,gBAAA,CADC,SAAS,EAAE,UAAU,KAAK,CAAC,CAAA,GA3BjB,eA4BX,WAAA,YAAA,CAAA;AAGA,gBAAA,CADC,MAAM,EAAE,aAAa,+BAA+B,CAAC,CAAA,GA9B3C,eA+BX,WAAA,aAAA,CAAA;AA/BW,iBAAN,gBAAA,CAbN,aAAa,EAAE,MAAM,WAAW,CAAC,GACjC,KAAK;CACJ,WAAW;CACX,iBAAiB,CAAC,cAAc,SAAS;CACzC,KAAK;EACH,SAAS;GAAC;GAAQ;GAAO;GAAU;EAAuB;EAC1D,QAAQ,EACN,uBAAuB;GAAE,QAAQ;GAAO,MAAM;EAAe,EAC/D;CACF;CACA,KAAK,EAAE,SAAS;EAAC;EAAQ;EAAO;CAAQ,EAAE;CAC1C,KAAK;AACP,CAAC,CAAA,GACY,cAAA;;;;AC1CN,IAAM,2BAAN,cAAuC,eAA+B;CAC3E,OAAgB,aAAa;CAErB,+BACN,UACA,qBACwC;EACxC,MAAM,iCAAiB,IAAI,IAAuC;EAClE,MAAM,WAAsB,MAAM,QAAQ,SAAS,SAAS,IACxD,SAAS,YACT,CAAC;EAEL,KAAA,MAAW,WAAW,UAAU;GAC9B,MAAM,OAAO;GAIb,MAAM,SACJ,OAAO,MAAM,WAAW,YAAY,KAAK,OAAO,SAAS,IACrD,KAAK,SACL;GACN,MAAM,eACJ,OAAO,MAAM,iBAAiB,YAAY,KAAK,aAAa,SAAS,IAChE,KAAK,eACN;GAEN,IAAI,CAAC,QACH;GAGF,eAAe,IAAI,cAAc,CAC/B,GAAI,eAAe,IAAI,YAAY,KAAK,CAAC,GACzC,MACF,CAAC;EACH;EAEA,IACE,eAAe,SAAS,KACxB,MAAM,QAAQ,SAAS,OAAO,KAC9B,SAAS,QAAQ,SAAS,GAE1B,eAAe,IACb,qBACA,SAAS,QAAQ,QACd,WACC,OAAO,WAAW,YAAY,OAAO,SAAS,CAClD,CACF;EAGF,OAAO;CACT;CAEA,MAAM,eAAe,WAA8C;EACjE,OAAO,KAAK,KAAK;GACf,OAAO,EAAE,UAAU;GACnB,SAAS;EACX,CAAC;CACH;CAEA,MAAM,oBAAoB,WAAmD;EAC3E,MAAM,WAAW,MAAM,KAAK,eAAe,SAAS;EACpD,OAAO,SAAS,SAAS,IAAI,SAAS,SAAS,SAAS,KAAK;CAC/D;CAEA,MAAM,6BACJ,WACgC;EAShC,QAAO,MARgB,KAAK,KAAK;GAC/B,OAAO;IACL;IACA,MAAM;GACR;GACA,SAAS;EACX,CAAC,EAAA,CAEe,MAAM;CACxB;CAEA,MAAM,WACJ,WACA,eACgC;EAChC,OAAO,KAAK,IAAI;GACd;GACA,SAAS;EACX,CAAC;CACH;CAEA,MAAM,qBAAqB,WAAoC;EAC7D,MAAM,SAAS,MAAM,KAAK,oBAAoB,SAAS;EACvD,OAAO,SAAS,OAAO,UAAU,IAAI;CACvC;CAEA,MAAM,eACJ,SACA,UAAuC,CAAC,GACf;EACzB,IAAI,CAAC,QAAQ,IACX,MAAM,IAAI,MAAM,6CAA6C;EAG/D,MAAM,UAAU,MAAM,KAAK,qBAAqB,QAAQ,EAAY;EACpE,MAAM,aAAa,MAAM,kCAAkC;GACzD,aAAa,QAAQ;GACrB,gBAAgB,QAAQ;GACxB,IAAI,KAAK;GACT,UAAU,QAAQ,YAAY;EAChC,CAAC;EACD,MAAM,CAAC,YAAY,gBAAgB,QAAQ,cAAc,MAAM,QAAQ,IAAI;GACzE,OAAO,QAAQ,kBAAkB,aAC7B,QAAQ,cAAc,IACtB,CAAC;GAGL,OAAO,QAAQ,sBAAsB,aACjC,QAAQ,kBAAkB,IAC1B,QAAQ,QAAQ,CAAC,CAAC;GACtB,OAAO,QAAQ,cAAc,aAAa,QAAQ,UAAU,IAAI,CAAC;GACjE,OAAO,QAAQ,kBAAkB,cACjC,WAAW,qBACP,QAAQ,cAAc,IACtB;IACE,SAAS,CAAC;IACV,OAAO,CAAC;IACR,WAAW,CAAC;GACd;EACN,CAAC;EAoCD,MAAM,WAAW;GAjCf,IAAI,QAAQ;GACZ,MAAM,QAAQ;GACd,SAAS,QAAQ;GACjB,MAAM,QAAQ;GACd,MAAM,QAAQ;GACd,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB,QAAQ,QAAQ;GAChB,OAAO,QAAQ;GACf,aAAa,QAAQ;GACrB,MAAM,QAAQ;GACd,YAAY,QAAQ;GACpB,cAAc,QAAQ;GACtB,KAAK,QAAQ;GACb,QAAQ,QAAQ;GAChB,cAAc,QAAQ;GACtB,UAAU,QAAQ;GAClB,MAAM,CAAC,GAAG,QAAQ,IAAI;GACtB,UAAU,QAAQ;GAClB,QAAQ,QAAQ;GAChB,OAAO,QAAQ;GACf,UAAU,QAAQ;GAClB,kBAAkB,QAAQ;GAC1B,cAAc,WAAW,KAAK,cAAc,UAAU,EAAE,CAAA,CAAE,OAAO,OAAO;GAGxE,gBAAgB,eAAe,QAAQ,SAAS,QAAQ,KAAK,QAAQ,CAAC;GACtE,UAAU,OAAO,KAAK,UAAU,MAAM,EAAE,CAAA,CAAE,OAAO,OAAO;GACxD,SAAS,WAAW;GACpB,WAAW,WAAW;GACtB,UAAU,QAAQ;GAClB,YAAY,QAAQ,OAAO,CAAA,CAAE;GAI7B,GAAI,QAAQ,YAAY,CAAC;EAC3B;EAOA,MAAM,cAAc,GALlB,SAAS,QACT,QAAQ,QACR,QAAQ,QACR,QAAQ,SACR,QAAQ,GAC4B,IAAK;EAE3C,OAAO,KAAK,OAAO;GACjB,MAAM;GACN,SAAS,QAAQ,WAAW;GAC5B,WAAW,QAAQ;GACnB;GACA,MAAM,QAAQ,QAAQ;GACtB,OAAO,SAAS,SAAS;GACzB,aAAa,SAAS,eAAe;GACrC,MAAM,SAAS,QAAQ;GACvB,QAAQ,SAAS,UAAU;GAC3B,SAAS,QAAQ,WAAW;GAC5B,UAAU,KAAK,UAAU,QAAQ;GACjC,UAAU,KAAK,UAAU,QAAQ,YAAY,CAAC,CAAC;GAC/C,UAAU,QAAQ;EACpB,CAAC;CACH;CAEA,MAAM,mBACJ,SACA,eACkB;EAClB,IAAI,CAAC,QAAQ,IACX,MAAM,IAAI,MAAM,wCAAwC;EAG1D,MAAM,UAAU,MAAM,KAAK,WAAW,QAAQ,IAAc,aAAa;EACzE,IAAI,CAAC,SACH,MAAM,IAAI,MACR,mBAAmB,cAAa,yBAA0B,QAAQ,IACpE;EAGF,MAAM,WAAW,QAAQ,YAAY;EACrC,MAAM,gBAAgB;GACpB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAKA,MAAM,kBAAkB;EACxB,KAAA,MAAW,OAAO,eAChB,IAAI,SAAS,SAAS,KAAA,GACpB,gBAAgB,OAAO,SAAS;EAUpC,MAAM,gBAGD,MAAM,QAAQ,SAAS,cAAc,IACrC,SAAS,eACP,QACE,SACC,CAAC,CAAC,QACF,OAAO,SAAS,YAChB,OAAQ,KAAgC,aAAa,YACpD,KAA8B,SAAS,SAAS,CACrD,CAAA,CACC,KAAK,UAAU;GACd,UAAU,KAAK;GACf,eACE,OAAO,KAAK,kBAAkB,WAC1B,KAAK,gBACL;EACR,EAAE,IACJ,MAAM,QAAQ,SAAS,YAAY,IACjC,SAAS,aACN,QACE,OACC,OAAO,OAAO,YAAY,GAAG,SAAS,CAC1C,CAAA,CACC,KAAK,cAAsB;GAAE;GAAU,eAAe;EAAK,EAAE,IAChE,CAAC;EAEP,IACE,MAAM,QAAQ,SAAS,cAAc,KACrC,MAAM,QAAQ,SAAS,YAAY,GAEnC,gBAAgB,eAAe,cAAc,KAAK,SAAS,KAAK,QAAQ;EAG1E,IAAI,MAAM,QAAQ,SAAS,QAAQ,GACjC,gBAAgB,WAAW,CAAC,GAAG,SAAS,QAAQ;EAGlD,MAAM,QAAQ,KAAK;EAiBnB,IACE,cAAc,SAAS,KACvB,OAAO,QAAQ,kBAAkB,cACjC,OAAO,QAAQ,iBAAiB,YAChC;GACA,MAAM,qBAAqB,MAAM,QAAQ,cAAc;GACvD,MAAM,eAAe,IAAI,IACvB,mBACG,QAAQ,cAAc,UAAU,EAAE,CAAA,CAClC,KAAK,cAAc,CAAC,UAAU,IAAc,SAAS,CAAC,CAC3D;GACA,KAAA,MAAW,QAAQ,eAAe;IAChC,MAAM,SAAS,aAAa,IAAI,KAAK,QAAQ;IAC7C,IAAI,QACF,MAAM,QAAQ,aAAa,QAAQ,EACjC,eAAe,KAAK,cACtB,CAAC;GAEL;EACF;EAEA,MAAM,aAAa,MAAM,kCAAkC;GACzD,aAAa,QAAQ;GACrB,gBAAgB,QAAQ;GACxB,IAAI,KAAK;GACT,UAAU,QAAQ,YAAY;EAChC,CAAC;EAED,IACE,WAAW,cACX,WAAW,sBACX,OAAO,QAAQ,iBAAiB,cAChC,OAAO,QAAQ,cAAc,YAC7B;GACA,MAAM,wBAAwB,KAAK,+BACjC,UACA,WAAW,uBACb;GACA,MAAM,eAAe,MAAM,QAAQ,aAAa;GAChD,MAAM,uBAAuB,IAAI,IAC/B,aAAa,KACV,SACE,KAAK,gBACN,WAAW,uBACf,CACF;GACA,MAAM,sCAAsB,IAAI,IAA6B,CAC3D,GAAG,sBACH,GAAG,sBAAsB,KAAK,CAChC,CAAC;GAED,KAAA,MAAW,gBAAgB,qBACzB,MAAM,QAAQ,UACZ,sBAAsB,IAAI,YAAY,KAAK,CAAC,GAC5C,YACF;EAEJ;EAEA,OAAO;CACT;AACF"}
|
|
1
|
+
{"version":3,"file":"content-versions-BnCiRXbb.js","names":[],"sources":["../../src/content-version.ts","../../src/content-versions.ts"],"sourcesContent":["import type { SmrtObjectOptions } from '@happyvertical/smrt-core';\nimport { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { ContentVersionKind } from './content-governance';\nimport { normalizeContentTransparency } from './content-transparency';\n\n/**\n * Shape of the loosely-structured `metadata` JSON bag stored on a content\n * version. Known fields are typed; unrecognized keys remain accessible via the\n * index signature.\n */\nexport interface ContentVersionMetadata {\n /**\n * Fingerprint of the snapshot captured for the latest publication version,\n * used to detect post-publication drift.\n */\n publicationSnapshotFingerprint?: string;\n [key: string]: unknown;\n}\n\nexport interface ContentVersionOptions extends SmrtObjectOptions {\n contentId?: string;\n version?: number;\n kind?: ContentVersionKind;\n title?: string;\n description?: string;\n body?: string;\n status?: string;\n summary?: string;\n snapshot?: string | Record<string, unknown>;\n metadata?: string | Record<string, unknown>;\n tenantId?: string | null;\n createdAt?: Date;\n updatedAt?: Date;\n}\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'content_versions',\n conflictColumns: ['content_id', 'version'],\n api: {\n include: ['list', 'get', 'create', 'getTransparencyAction'],\n routes: {\n getTransparencyAction: { method: 'GET', path: 'transparency' },\n },\n },\n mcp: { include: ['list', 'get', 'create'] },\n cli: true,\n})\nexport class ContentVersion extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-content\";\n @foreignKey('Content', { required: true, onDelete: 'CASCADE' })\n @field({ description: 'The piece this version belongs to.' })\n contentId = '';\n\n @field({ description: 'The number of this version, counting up from one.' })\n version = 1;\n\n @field({\n description:\n 'Why the version was saved: a draft, a review, a publication or a correction.',\n })\n kind: ContentVersionKind = 'manual';\n\n @field({ description: 'The headline as it was in this version.' })\n title = '';\n description = '';\n body = '';\n\n @field({ description: 'Where the piece stood when this version was saved.' })\n status = 'draft';\n\n @field({ description: 'A short note on what changed in this version.' })\n summary = '';\n snapshot = '{}';\n metadata = '';\n\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @field({ description: 'When this version was saved.' })\n createdAt = new Date();\n updatedAt = new Date();\n\n constructor(options: ContentVersionOptions = {}) {\n super(options);\n if (options.contentId) this.contentId = options.contentId;\n if (options.version !== undefined) this.version = options.version;\n if (options.kind !== undefined) this.kind = options.kind;\n if (options.title !== undefined) this.title = options.title;\n if (options.description !== undefined)\n this.description = options.description;\n if (options.body !== undefined) this.body = options.body;\n if (options.status !== undefined) this.status = options.status;\n if (options.summary !== undefined) this.summary = options.summary;\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.createdAt) this.createdAt = options.createdAt;\n if (options.updatedAt) this.updatedAt = options.updatedAt;\n\n if (options.snapshot !== undefined) {\n this.snapshot =\n typeof options.snapshot === 'string'\n ? options.snapshot\n : JSON.stringify(options.snapshot);\n }\n\n if (options.metadata !== undefined) {\n this.metadata =\n typeof options.metadata === 'string'\n ? options.metadata\n : JSON.stringify(options.metadata);\n }\n }\n\n getSnapshot(): Record<string, unknown> {\n try {\n return this.snapshot ? JSON.parse(this.snapshot) : {};\n } catch {\n return {};\n }\n }\n\n getMetadata(): ContentVersionMetadata {\n try {\n return this.metadata ? JSON.parse(this.metadata) : {};\n } catch {\n return {};\n }\n }\n\n getTransparency() {\n const metadata = this.getMetadata();\n const snapshot = this.getSnapshot();\n\n return normalizeContentTransparency(metadata.transparency, {\n snapshotKind: this.kind === 'publication' ? 'published' : 'preview',\n contentId: this.contentId || (snapshot.contentId as string) || null,\n currentContentStatus:\n this.status || (snapshot.status as string) || 'draft',\n publicationVersion: {\n id: (this.id as string) || null,\n version: this.version ?? null,\n kind: this.kind || null,\n summary: this.summary || '',\n createdAt:\n this.createdAt instanceof Date ? this.createdAt.toISOString() : null,\n },\n });\n }\n\n async getTransparencyAction() {\n return this.getTransparency();\n }\n}\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport type { FactContentRelationship } from '@happyvertical/smrt-facts';\nimport type { Content } from './content';\nimport type { CreateContentVersionOptions } from './content-governance';\nimport { resolveEffectiveContentGovernance } from './content-governance';\nimport { ContentVersion } from './content-version';\n\nexport class ContentVersionCollection extends SmrtCollection<ContentVersion> {\n static readonly _itemClass = ContentVersion;\n\n private buildSnapshotFactRelationships(\n snapshot: Record<string, unknown>,\n defaultRelationship: FactContentRelationship,\n ): Map<FactContentRelationship, string[]> {\n const byRelationship = new Map<FactContentRelationship, string[]>();\n const rawLinks: unknown[] = Array.isArray(snapshot.factLinks)\n ? snapshot.factLinks\n : [];\n\n for (const rawLink of rawLinks) {\n const link = rawLink as {\n factId?: unknown;\n relationship?: unknown;\n } | null;\n const factId =\n typeof link?.factId === 'string' && link.factId.length > 0\n ? link.factId\n : null;\n const relationship =\n typeof link?.relationship === 'string' && link.relationship.length > 0\n ? (link.relationship as FactContentRelationship)\n : defaultRelationship;\n\n if (!factId) {\n continue;\n }\n\n byRelationship.set(relationship, [\n ...(byRelationship.get(relationship) || []),\n factId,\n ]);\n }\n\n if (\n byRelationship.size === 0 &&\n Array.isArray(snapshot.factIds) &&\n snapshot.factIds.length > 0\n ) {\n byRelationship.set(\n defaultRelationship,\n snapshot.factIds.filter(\n (factId: unknown): factId is string =>\n typeof factId === 'string' && factId.length > 0,\n ),\n );\n }\n\n return byRelationship;\n }\n\n async listForContent(contentId: string): Promise<ContentVersion[]> {\n return this.list({\n where: { contentId },\n orderBy: 'version ASC',\n });\n }\n\n async getLatestForContent(contentId: string): Promise<ContentVersion | null> {\n const versions = await this.listForContent(contentId);\n return versions.length > 0 ? versions[versions.length - 1] : null;\n }\n\n async getLatestPublishedForContent(\n contentId: string,\n ): Promise<ContentVersion | null> {\n const versions = await this.list({\n where: {\n contentId,\n kind: 'publication',\n },\n orderBy: 'version DESC',\n });\n\n return versions[0] || null;\n }\n\n async getVersion(\n contentId: string,\n versionNumber: number,\n ): Promise<ContentVersion | null> {\n return this.get({\n contentId,\n version: versionNumber,\n });\n }\n\n async getNextVersionNumber(contentId: string): Promise<number> {\n const latest = await this.getLatestForContent(contentId);\n return latest ? latest.version + 1 : 1;\n }\n\n async createSnapshot(\n content: Content,\n options: CreateContentVersionOptions = {},\n ): Promise<ContentVersion> {\n if (!content.id) {\n throw new Error('Cannot create a version for unsaved content');\n }\n\n const version = await this.getNextVersionNumber(content.id as string);\n const governance = await resolveEffectiveContentGovernance({\n contentType: content.type,\n contentVariant: content.variant,\n db: this.db,\n tenantId: content.tenantId ?? null,\n });\n const [references, referenceEdges, assets, factsState] = await Promise.all([\n typeof content.getReferences === 'function'\n ? content.getReferences()\n : [],\n // Capture per-edge citation pins so restore can reconstruct them.\n // `getReferences()` resolves to Content objects and loses targetVersion.\n typeof content.getReferenceEdges === 'function'\n ? content.getReferenceEdges()\n : Promise.resolve([]),\n typeof content.getAssets === 'function' ? content.getAssets() : [],\n typeof content.getFactsState === 'function' &&\n governance.factLinkingEnabled\n ? content.getFactsState()\n : {\n factIds: [],\n facts: [],\n factLinks: [],\n },\n ]);\n\n const baseSnapshot = {\n id: content.id,\n slug: content.slug,\n context: content.context,\n name: content.name,\n type: content.type,\n variant: content.variant,\n fileKey: content.fileKey,\n author: content.author,\n title: content.title,\n description: content.description,\n body: content.body,\n bodyFormat: content.bodyFormat,\n publish_date: content.publish_date,\n url: content.url,\n source: content.source,\n original_url: content.original_url,\n language: content.language,\n tags: [...content.tags],\n category: content.category,\n status: content.status,\n state: content.state,\n metadata: content.metadata,\n thumbnailAssetId: content.thumbnailAssetId,\n referenceIds: references.map((reference) => reference.id).filter(Boolean),\n // Full edges with citation pins; `referenceIds` retained for back-compat\n // with snapshots written before pin-aware restore (#1387 #3).\n referenceEdges: referenceEdges.filter((edge) => Boolean(edge.targetId)),\n assetIds: assets.map((asset) => asset.id).filter(Boolean),\n factIds: factsState.factIds,\n factLinks: factsState.factLinks,\n tenantId: content.tenantId,\n _meta_type: content.toJSON()._meta_type,\n };\n const snapshot = {\n ...baseSnapshot,\n ...(options.snapshot || {}),\n };\n const versionSlugBase =\n snapshot.slug ||\n content.slug ||\n content.name ||\n content.title ||\n content.id;\n const versionSlug = `${versionSlugBase}-v${version}`;\n\n return this.create({\n slug: versionSlug,\n context: content.context || '',\n contentId: content.id as string,\n version,\n kind: options.kind || 'manual',\n title: snapshot.title || '',\n description: snapshot.description || '',\n body: snapshot.body || '',\n status: snapshot.status || 'draft',\n summary: options.summary || '',\n snapshot: JSON.stringify(snapshot),\n metadata: JSON.stringify(options.metadata || {}),\n tenantId: content.tenantId,\n });\n }\n\n async restoreIntoContent(\n content: Content,\n versionNumber: number,\n ): Promise<Content> {\n if (!content.id) {\n throw new Error('Cannot restore an unsaved content item');\n }\n\n const version = await this.getVersion(content.id as string, versionNumber);\n if (!version) {\n throw new Error(\n `Content version ${versionNumber} not found for content ${content.id}`,\n );\n }\n\n const snapshot = version.getSnapshot();\n const keysToRestore = [\n 'name',\n 'type',\n 'variant',\n 'fileKey',\n 'author',\n 'title',\n 'description',\n 'body',\n 'bodyFormat',\n 'publish_date',\n 'url',\n 'source',\n 'original_url',\n 'language',\n 'tags',\n 'category',\n 'status',\n 'state',\n 'metadata',\n 'thumbnailAssetId',\n ];\n\n // Restore snapshot values onto the live Content instance by field name.\n // Indexing a class instance by an arbitrary string key requires a record\n // view; the keys are a fixed, known set of Content fields.\n const writableContent = content as unknown as Record<string, unknown>;\n for (const key of keysToRestore) {\n if (snapshot[key] !== undefined) {\n writableContent[key] = snapshot[key];\n }\n }\n\n // Reference edges with citation pins (#1387 #3). Newer snapshots carry\n // `referenceEdges` ({ targetId, targetVersion }); older ones only have\n // `referenceIds`. Either way, seed the pending `referenceIds` from the\n // target ids so `save()` reconciles the set (adds missing, removes extra),\n // then re-apply the saved pins below so restoring \"to vN\" reconstructs the\n // citation pins that existed at vN instead of dropping them to unpinned.\n const snapshotEdges: Array<{\n targetId: string;\n targetVersion: number | null;\n }> = Array.isArray(snapshot.referenceEdges)\n ? (snapshot.referenceEdges as unknown[])\n .filter(\n (edge): edge is { targetId: string; targetVersion?: unknown } =>\n !!edge &&\n typeof edge === 'object' &&\n typeof (edge as { targetId?: unknown }).targetId === 'string' &&\n (edge as { targetId: string }).targetId.length > 0,\n )\n .map((edge) => ({\n targetId: edge.targetId,\n targetVersion:\n typeof edge.targetVersion === 'number'\n ? edge.targetVersion\n : null,\n }))\n : Array.isArray(snapshot.referenceIds)\n ? snapshot.referenceIds\n .filter(\n (id: unknown): id is string =>\n typeof id === 'string' && id.length > 0,\n )\n .map((targetId: string) => ({ targetId, targetVersion: null }))\n : [];\n\n if (\n Array.isArray(snapshot.referenceEdges) ||\n Array.isArray(snapshot.referenceIds)\n ) {\n writableContent.referenceIds = snapshotEdges.map((edge) => edge.targetId);\n }\n\n if (Array.isArray(snapshot.assetIds)) {\n writableContent.assetIds = [...snapshot.assetIds];\n }\n\n await content.save();\n\n // Re-apply the citation pin of EVERY snapshot edge — including UNPINNED\n // ones (`targetVersion: null`). `save()` only reconciles the target-id set\n // (adds missing / removes extra edges) and leaves the pin of an edge that\n // already existed untouched. So restoring an *unpinned* snapshot over an\n // edge that is currently *pinned* must explicitly clear that pin, otherwise\n // the live pin survives the restore and drift never resets. Passing\n // `addReference(target, { targetVersion: null })` clears the pin in place\n // (the junction's `attach` updates the row when `null !== existing`), while\n // a non-null value (re)sets it — so \"restore to vN\" reconstructs exactly\n // the pins that existed at vN.\n //\n // We pass the resolved Content object (not the raw id) because\n // `addReference(string)` treats the string as a URL, not a content id.\n // `addReference` is idempotent on (source, target) and only adjusts\n // targetVersion.\n if (\n snapshotEdges.length > 0 &&\n typeof content.getReferences === 'function' &&\n typeof content.addReference === 'function'\n ) {\n const resolvedReferences = await content.getReferences();\n const resolvedById = new Map(\n resolvedReferences\n .filter((reference) => reference.id)\n .map((reference) => [reference.id as string, reference]),\n );\n for (const edge of snapshotEdges) {\n const target = resolvedById.get(edge.targetId);\n if (target) {\n await content.addReference(target, {\n targetVersion: edge.targetVersion,\n });\n }\n }\n }\n\n const governance = await resolveEffectiveContentGovernance({\n contentType: content.type,\n contentVariant: content.variant,\n db: this.db,\n tenantId: content.tenantId ?? null,\n });\n\n if (\n governance.isGoverned &&\n governance.factLinkingEnabled &&\n typeof content.getFactLinks === 'function' &&\n typeof content.syncFacts === 'function'\n ) {\n const desiredByRelationship = this.buildSnapshotFactRelationships(\n snapshot,\n governance.defaultFactRelationship,\n );\n const currentLinks = await content.getFactLinks();\n const currentRelationships = new Set(\n currentLinks.map(\n (link) =>\n (link.relationship as FactContentRelationship) ||\n governance.defaultFactRelationship,\n ),\n );\n const relationshipsToSync = new Set<FactContentRelationship>([\n ...currentRelationships,\n ...desiredByRelationship.keys(),\n ]);\n\n for (const relationship of relationshipsToSync) {\n await content.syncFacts(\n desiredByRelationship.get(relationship) || [],\n relationship,\n );\n }\n }\n\n return content;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { $ as loadPersistedContentGovernanceDefinitions, D as serializeFact, E as serializeContent, Y as getEffectiveContentGovernanceConfig, rt as resolveEffectiveContentGovernance, w as Content, x as executeContentQuery } from "./content-query-
|
|
2
|
+
import { $ as loadPersistedContentGovernanceDefinitions, D as serializeFact, E as serializeContent, Y as getEffectiveContentGovernanceConfig, rt as resolveEffectiveContentGovernance, w as Content, x as executeContentQuery } from "./content-query-DcVNRGoy.js";
|
|
3
3
|
import { htmlToMarkdown, resolveBodyFormat } from "../body-format.js";
|
|
4
4
|
import { SmrtCollection, smrt } from "@happyvertical/smrt-core";
|
|
5
5
|
import { createLogger } from "@happyvertical/logger";
|
|
@@ -816,4 +816,4 @@ Contents = __decorateClass([smrt({
|
|
|
816
816
|
//#endregion
|
|
817
817
|
export { fetchSafeRemoteUrlInternal as i, contents_exports as n, fetchSafeRemoteUrl as r, Contents as t };
|
|
818
818
|
|
|
819
|
-
//# sourceMappingURL=contents-
|
|
819
|
+
//# sourceMappingURL=contents-DDiybAlF.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contents-CdF4fURL.js","names":["body"],"sources":["../../src/safe-remote-url.ts","../../src/contents.ts"],"sourcesContent":["/**\n * Shared SSRF guard for content's outbound fetches (feed sync, mirroring).\n *\n * Any code path that fetches a caller-supplied URL — RSS/Atom feeds, the\n * `Mirror` content type, link previews — is an SSRF vector: an attacker who can\n * influence the URL can make the server fetch `169.254.169.254` cloud metadata,\n * `localhost` admin panels, or other internal services. This module rejects\n * URLs that resolve to private, loopback, link-local, CGNAT, or cloud-metadata\n * ranges before any request is made, and re-validates redirect hops.\n *\n * Unlike a literal-IP-only check, {@link assertSafeRemoteUrl} resolves hostnames\n * via DNS so a public name pointing at a private IP is also caught (S5 #1388).\n */\nimport { lookup as dnsLookup } from 'node:dns/promises';\nimport { isIP } from 'node:net';\nimport { Agent, fetch as undiciFetch } from 'undici';\n\nexport type ResolvedAddress = { address: string; family?: number };\nexport type ResolveHostname = (hostname: string) => Promise<ResolvedAddress[]>;\n\nexport interface SafeRemoteUrlOptions {\n /** Skip the private-network checks (trusted callers only, e.g. local dev). */\n allowPrivateNetworkHosts?: boolean;\n /** Injectable resolver for tests; defaults to {@link defaultResolveHostname}. */\n resolveHostname?: ResolveHostname;\n}\n\nexport interface ValidatedRemoteUrlOptions extends SafeRemoteUrlOptions {\n /** Resolve even when private hosts are explicitly allowed, for pinned fetches. */\n resolveHostnameWhenPrivateAllowed?: boolean;\n}\n\n/** A URL together with the exact addresses accepted by its DNS validation. */\nexport interface ValidatedRemoteUrl {\n url: URL;\n addresses: ResolvedAddress[];\n}\n\n/** Options for bounded, SSRF-safe HTTPS retrieval. */\nexport interface SafeRemoteFetchOptions {\n /** Only Accept, Accept-Language, If-Modified-Since, If-None-Match, and User-Agent. */\n headers?: Record<string, string>;\n /** Total DNS, request, redirect, and response-body deadline. Defaults to 10s. */\n timeoutMs?: number;\n /** Maximum returned response-body bytes. Defaults to 2 MB (2,000,000 bytes). */\n maxBytes?: number;\n /** Maximum validated redirect hops. Defaults to 5. Set 0 to reject redirects. */\n maxRedirects?: number;\n /** Injectable DNS resolver. Its results are still validated and pinned. */\n resolveHostname?: ResolveHostname;\n}\n\n/** A fully buffered response from {@link fetchSafeRemoteUrl}. */\nexport interface SafeRemoteFetchResult {\n /** Final URL after validated redirects. */\n url: URL;\n status: number;\n statusText: string;\n ok: boolean;\n /** Lower-cased response header names. */\n headers: Readonly<Record<string, string>>;\n /** Bounded response bytes. */\n body: Uint8Array;\n}\n\nexport async function defaultResolveHostname(\n hostname: string,\n): Promise<ResolvedAddress[]> {\n return dnsLookup(hostname, { all: true, verbatim: false });\n}\n\nexport function isBlockedIPv4(address: string): boolean {\n const parts = address.split('.');\n if (parts.length !== 4) return true;\n // Strict per-octet validation: `Number('')` is 0, so without a digit check a\n // malformed input like `1..2.3` would parse to `[1,0,2,3]` and be treated as a\n // valid (and possibly allowed) address. Anything not 1-3 digits in 0-255 is\n // unparseable → blocked (review #1562).\n const octets = parts.map((part) =>\n /^\\d{1,3}$/.test(part) ? Number(part) : Number.NaN,\n );\n if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {\n return true;\n }\n\n const [first, second, third] = octets;\n return (\n first === 0 ||\n first === 10 ||\n first === 127 ||\n first >= 224 ||\n (first === 100 && second >= 64 && second <= 127) ||\n (first === 169 && second === 254) ||\n (first === 172 && second >= 16 && second <= 31) ||\n (first === 192 && second === 168) ||\n (first === 192 && second === 0 && (third === 0 || third === 2)) ||\n (first === 192 && second === 88 && third === 99) ||\n (first === 198 && (second === 18 || second === 19)) ||\n (first === 198 && second === 51 && third === 100) ||\n (first === 203 && second === 0 && third === 113)\n );\n}\n\n/**\n * Expand an IPv6 string to its 8 hextets (numbers), or null if unparseable.\n * Handles `::` compression, an embedded dotted-IPv4 tail, and bracketed forms.\n */\nfunction expandIPv6(address: string): number[] | null {\n let work = address.toLowerCase().replace(/^\\[|\\]$/g, '');\n if (work.includes('.')) {\n // Embedded dotted IPv4 tail (e.g. ::ffff:127.0.0.1) → convert to 2 hextets.\n const m = work.match(/(\\d{1,3}(?:\\.\\d{1,3}){3})$/);\n if (!m) return null;\n const o = m[1].split('.').map(Number);\n if (o.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;\n const hi = ((o[0] << 8) | o[1]).toString(16);\n const lo = ((o[2] << 8) | o[3]).toString(16);\n work = `${work.slice(0, work.length - m[1].length)}${hi}:${lo}`;\n }\n const halves = work.split('::');\n if (halves.length > 2) return null;\n const head = halves[0] ? halves[0].split(':') : [];\n const tail = halves.length === 2 && halves[1] ? halves[1].split(':') : [];\n const groups =\n halves.length === 2\n ? [\n ...head,\n ...Array(Math.max(0, 8 - head.length - tail.length)).fill('0'),\n ...tail,\n ]\n : head;\n if (groups.length !== 8) return null;\n const hextets = groups.map((g) =>\n /^[0-9a-f]{1,4}$/.test(g) ? Number.parseInt(g, 16) : Number.NaN,\n );\n return hextets.some((n) => Number.isNaN(n)) ? null : hextets;\n}\n\nexport function isBlockedIPv6(address: string): boolean {\n const hextets = expandIPv6(address);\n if (hextets) {\n // IPv4-mapped (::ffff:a.b.c.d) addresses embed an IPv4 in the last two\n // hextets — decode and apply the IPv4\n // blocklist so a loopback/private IPv4 can't be smuggled through any IPv6\n // encoding (compressed, expanded, dotted, or hex) (review #1562, P1).\n const firstFiveZero = hextets.slice(0, 5).every((h) => h === 0);\n if (firstFiveZero && hextets[5] === 0xffff) {\n const [, , , , , , g6, g7] = hextets;\n const ipv4 = `${g6 >> 8}.${g6 & 0xff}.${g7 >> 8}.${g7 & 0xff}`;\n return isBlockedIPv4(ipv4);\n }\n }\n\n // Conservatively allow global unicast only, excluding IETF special-purpose,\n // documentation, and transition ranges. Compare parsed hextets so expanded\n // spellings cannot bypass the guard. IPv4-mapped addresses are checked above.\n // https://www.iana.org/assignments/iana-ipv6-special-registry/\n if (!hextets) return true;\n return (\n (hextets[0] & 0xe000) !== 0x2000 ||\n (hextets[0] === 0x2001 && hextets[1] < 0x0200) ||\n (hextets[0] === 0x2001 && hextets[1] === 0x0db8) ||\n hextets[0] === 0x2002 ||\n (hextets[0] === 0x3fff && hextets[1] < 0x1000)\n );\n}\n\nexport function isBlockedAddress(address: string): boolean {\n const family = isIP(address);\n if (family === 4) return isBlockedIPv4(address);\n if (family === 6) return isBlockedIPv6(address);\n // Anything that isn't a recognisable IP literal (after DNS resolution should\n // have produced one) is treated as unsafe.\n return true;\n}\n\n/**\n * Parse and validate a remote URL for outbound fetching. Rejects non-http(s)\n * schemes, embedded credentials, and hosts that resolve to non-public ranges.\n * Returns the parsed URL and the exact accepted addresses so callers can bind\n * their connection without resolving the hostname again.\n */\nexport async function validateSafeRemoteUrl(\n rawUrl: string,\n options: ValidatedRemoteUrlOptions = {},\n): Promise<ValidatedRemoteUrl> {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new Error('Remote URL must be an absolute URL');\n }\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new Error('Remote URL must use http or https');\n }\n if (url.username || url.password) {\n throw new Error('Remote URL must not include credentials');\n }\n if (!url.hostname) {\n throw new Error('Remote URL must include a hostname');\n }\n\n if (\n options.allowPrivateNetworkHosts &&\n !options.resolveHostnameWhenPrivateAllowed\n ) {\n return { url, addresses: [] };\n }\n\n const resolver = options.resolveHostname ?? defaultResolveHostname;\n const addresses =\n isIP(url.hostname) === 0\n ? await resolver(url.hostname)\n : [{ address: url.hostname }];\n\n if (\n !addresses.length ||\n (!options.allowPrivateNetworkHosts &&\n addresses.some(({ address }) => isBlockedAddress(address)))\n ) {\n throw new Error('Remote URL must resolve to a public network address');\n }\n\n return { url, addresses };\n}\n\nexport async function assertSafeRemoteUrl(\n rawUrl: string,\n options: SafeRemoteUrlOptions = {},\n): Promise<URL> {\n return (await validateSafeRemoteUrl(rawUrl, options)).url;\n}\n\n/** Redirect status codes that reroute a request to a new Location. */\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\nconst DEFAULT_MAX_REDIRECTS = 5;\nconst DEFAULT_RESOLVE_TIMEOUT_MS = 10_000;\n\nexport interface SafeRedirectOptions extends SafeRemoteUrlOptions {\n /** Maximum redirect hops to follow before failing. Default 5. */\n maxRedirects?: number;\n /** Per-hop timeout in ms. Default 10s. */\n timeoutMs?: number;\n /** Injectable fetch (primarily for tests). Defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Resolve a URL's redirect chain and return the final safe {@link URL}, with\n * EVERY hop re-validated through {@link assertSafeRemoteUrl}.\n *\n * Use this before handing a URL to a fetcher that follows redirects on its own\n * (e.g. `fetchDocument`): the up-front {@link assertSafeRemoteUrl} check alone\n * can't stop an allowed public host from `30x`-redirecting into an internal /\n * loopback / metadata host, which would defeat the SSRF guard (review #1562).\n *\n * Redirects are followed with `GET` + `redirect: 'manual'` to match downstream\n * GET-based fetchers; response bodies are discarded (the redirect bodies are\n * empty and the terminal body is left for the caller to re-fetch), so this does\n * not double-download content.\n */\nexport async function resolveSafeFinalUrl(\n rawUrl: string,\n options: SafeRedirectOptions = {},\n): Promise<URL> {\n const fetchImpl = options.fetchImpl ?? fetch;\n const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;\n const timeoutMs = options.timeoutMs ?? DEFAULT_RESOLVE_TIMEOUT_MS;\n let current = await assertSafeRemoteUrl(rawUrl, options);\n\n for (let hop = 0; hop <= maxRedirects; hop += 1) {\n const response = await fetchImpl(current, {\n method: 'GET',\n redirect: 'manual',\n signal: AbortSignal.timeout(timeoutMs),\n });\n // Release the connection without downloading the body.\n try {\n await response.body?.cancel();\n } catch {\n // best-effort\n }\n\n if (!REDIRECT_STATUSES.has(response.status)) {\n return current;\n }\n const location = response.headers.get('location');\n if (!location) return current;\n // Re-validate the resolved redirect target before following it.\n current = await assertSafeRemoteUrl(\n new URL(location, current).toString(),\n options,\n );\n }\n\n throw new Error('Remote URL exceeded the maximum number of redirects');\n}\n\n/**\n * Strip userinfo (`user:pass@`) from a URL so it can be safely logged or echoed\n * in an error message. Returns a placeholder for unparseable input. Never let a\n * credential-bearing URL reach logs/errors verbatim (review #1562).\n */\nexport function redactUrlCredentials(raw: string): string {\n try {\n const url = new URL(raw);\n if (url.username || url.password) {\n url.username = '';\n url.password = '';\n return url.toString();\n }\n return raw;\n } catch {\n return '[unparseable url]';\n }\n}\n\nconst DEFAULT_MAX_RESPONSE_BYTES = 2_000_000;\nconst DEFAULT_FETCH_TIMEOUT_MS = 10_000;\nconst SAFE_REQUEST_HEADER_NAMES = new Set([\n 'accept',\n 'accept-language',\n 'if-modified-since',\n 'if-none-match',\n 'user-agent',\n]);\n\nfunction assertSafeRequestHeaders(headers: Record<string, string> | undefined) {\n for (const name of Object.keys(headers ?? {})) {\n if (!SAFE_REQUEST_HEADER_NAMES.has(name.toLowerCase())) {\n throw new Error(\n `Safe remote fetch does not allow request header ${name}`,\n );\n }\n }\n}\n\ntype RemoteResponse = {\n status: number;\n statusText: string;\n ok: boolean;\n headers: {\n get: (name: string) => string | null;\n forEach: (callback: (value: string, key: string) => void) => void;\n };\n body: {\n locked: boolean;\n cancel: (reason?: unknown) => Promise<void>;\n getReader: () => {\n cancel: (reason?: unknown) => Promise<void>;\n read: () => Promise<{ done: boolean; value?: Uint8Array }>;\n releaseLock: () => void;\n };\n } | null;\n arrayBuffer: () => Promise<ArrayBuffer>;\n};\n\ntype RemoteFetchResult = {\n response: RemoteResponse;\n close: () => Promise<void>;\n};\n\ninterface InternalSafeRemoteFetchOptions extends SafeRemoteFetchOptions {\n allowHttp?: boolean;\n allowPrivateNetworkHosts?: boolean;\n fetchImpl?: typeof fetch;\n}\n\nfunction createPinnedDispatcher(addresses: ResolvedAddress[]): Agent {\n const pinnedAddresses = addresses.map(({ address, family }) => {\n const resolvedFamily = family ?? isIP(address);\n if (resolvedFamily !== 4 && resolvedFamily !== 6) {\n throw new Error('Remote URL resolved to an unrecognised network address');\n }\n return { address, family: resolvedFamily };\n });\n\n return new Agent({\n connect: {\n lookup: (_hostname, lookupOptions, callback) => {\n if (lookupOptions.all) {\n callback(null, pinnedAddresses);\n return;\n }\n const selected =\n pinnedAddresses.find(\n ({ family }) =>\n !lookupOptions.family || family === lookupOptions.family,\n ) ?? pinnedAddresses[0];\n callback(null, selected.address, selected.family);\n },\n },\n });\n}\n\nasync function fetchPinnedRemoteUrl(\n url: ValidatedRemoteUrl,\n options: InternalSafeRemoteFetchOptions,\n signal: AbortSignal | undefined,\n): Promise<RemoteFetchResult> {\n const init = {\n headers: options.headers,\n redirect: 'manual' as const,\n signal,\n };\n // This is an internal trusted test/embedding seam. Public callers use the\n // Undici transport below, which cannot resolve the hostname a second time.\n if (options.fetchImpl) {\n return {\n response: await options.fetchImpl(url.url, init),\n close: async () => {},\n };\n }\n const dispatcher = createPinnedDispatcher(url.addresses);\n try {\n return {\n response: await undiciFetch(url.url, { ...init, dispatcher }),\n close: () => dispatcher.close(),\n };\n } catch (error) {\n await dispatcher.close();\n throw error;\n }\n}\n\nasync function closeRemoteResponse({ response, close }: RemoteFetchResult) {\n try {\n if (response.body && !response.body.locked) await response.body.cancel();\n } catch {\n // A failed stream may reject cancellation; the dispatcher still closes.\n } finally {\n await close();\n }\n}\n\nasync function readBoundedResponseBytes(\n response: RemoteResponse,\n maxBytes: number,\n): Promise<Uint8Array> {\n // A 304 has no message body; its Content-Length describes the selected\n // representation, not bytes transferred in this response.\n if (response.status === 304) return new Uint8Array();\n const contentLength = response.headers.get('content-length');\n if (contentLength && Number(contentLength) > maxBytes) {\n throw new Error(`Remote response exceeds ${maxBytes} bytes`);\n }\n if (!response.body) {\n const body = new Uint8Array(await response.arrayBuffer());\n if (body.byteLength > maxBytes)\n throw new Error(`Remote response exceeds ${maxBytes} bytes`);\n return body;\n }\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (!value) continue;\n totalBytes += value.byteLength;\n if (totalBytes > maxBytes) {\n await reader.cancel();\n throw new Error(`Remote response exceeds ${maxBytes} bytes`);\n }\n chunks.push(value);\n }\n } catch (error) {\n await reader.cancel(error);\n throw error;\n } finally {\n reader.releaseLock();\n }\n const body = new Uint8Array(totalBytes);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return body;\n}\n\nfunction copyResponseHeaders(\n headers: RemoteResponse['headers'],\n): Readonly<Record<string, string>> {\n const result: Record<string, string> = {};\n headers.forEach((value, key) => {\n result[key.toLowerCase()] = value;\n });\n return Object.freeze(result);\n}\n\nfunction removeCrossOriginSensitiveHeaders(\n headers: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n if (!headers) return undefined;\n const safeHeaders = Object.fromEntries(\n Object.entries(headers).filter(\n ([name]) =>\n !['authorization', 'cookie', 'proxy-authorization'].includes(\n name.toLowerCase(),\n ),\n ),\n );\n return safeHeaders;\n}\n\nfunction assertFetchLimits(options: InternalSafeRemoteFetchOptions) {\n const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;\n if (!Number.isFinite(timeoutMs) || timeoutMs < 0)\n throw new Error(\n 'Remote fetch timeout must be a non-negative finite number',\n );\n if (!Number.isInteger(maxBytes) || maxBytes < 0)\n throw new Error('Remote fetch maxBytes must be a non-negative integer');\n if (!Number.isInteger(maxRedirects) || maxRedirects < 0)\n throw new Error('Remote fetch maxRedirects must be a non-negative integer');\n return { timeoutMs, maxBytes, maxRedirects };\n}\n\n/**\n * Fetch an untrusted remote HTTPS URL through a DNS-pinned transport.\n * Redirect targets are independently validated, and returned bodies are fully\n * buffered within `maxBytes`; callers never receive a live response stream.\n */\nexport async function fetchSafeRemoteUrl(\n rawUrl: string,\n options: SafeRemoteFetchOptions = {},\n): Promise<SafeRemoteFetchResult> {\n if (options.timeoutMs === 0) {\n throw new Error('Safe remote fetch timeout must be greater than zero');\n }\n assertSafeRequestHeaders(options.headers);\n return fetchSafeRemoteUrlInternal(rawUrl, options);\n}\n\n/** @internal Shared feed transport; its HTTP/private/test seams are not exported from the package barrel. */\nexport async function fetchSafeRemoteUrlInternal(\n rawUrl: string,\n options: InternalSafeRemoteFetchOptions = {},\n): Promise<SafeRemoteFetchResult> {\n const { timeoutMs, maxBytes, maxRedirects } = assertFetchLimits(options);\n const controller = timeoutMs > 0 ? new AbortController() : undefined;\n let deadline: ReturnType<typeof setTimeout> | undefined;\n const timeout =\n timeoutMs > 0\n ? new Promise<never>((_resolve, reject) => {\n deadline = setTimeout(() => {\n controller?.abort();\n reject(new Error(`Remote fetch timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n })\n : undefined;\n const run = async (): Promise<SafeRemoteFetchResult> => {\n let current = await validateSafeRemoteUrl(rawUrl, {\n allowPrivateNetworkHosts: options.allowPrivateNetworkHosts,\n resolveHostnameWhenPrivateAllowed: true,\n resolveHostname: options.resolveHostname,\n });\n let requestHeaders = options.headers;\n if (!options.allowHttp && current.url.protocol !== 'https:') {\n throw new Error('Remote fetch URL must use https');\n }\n for (let hop = 0; hop <= maxRedirects; hop += 1) {\n const result = await fetchPinnedRemoteUrl(\n current,\n {\n ...options,\n headers: requestHeaders,\n },\n controller?.signal,\n );\n const { response } = result;\n if (REDIRECT_STATUSES.has(response.status)) {\n const location = response.headers.get('location');\n if (!location) {\n await closeRemoteResponse(result);\n throw new Error('Remote redirect response missing Location header');\n }\n try {\n const previousOrigin = current.url.origin;\n current = await validateSafeRemoteUrl(\n new URL(location, current.url).toString(),\n {\n allowPrivateNetworkHosts: options.allowPrivateNetworkHosts,\n resolveHostnameWhenPrivateAllowed: true,\n resolveHostname: options.resolveHostname,\n },\n );\n if (!options.allowHttp && current.url.protocol !== 'https:') {\n throw new Error('Remote fetch URL must use https');\n }\n if (current.url.origin !== previousOrigin) {\n requestHeaders = removeCrossOriginSensitiveHeaders(requestHeaders);\n }\n } finally {\n await closeRemoteResponse(result);\n }\n continue;\n }\n try {\n return {\n url: current.url,\n status: response.status,\n statusText: response.statusText,\n ok: response.ok,\n headers: copyResponseHeaders(response.headers),\n body: await readBoundedResponseBytes(response, maxBytes),\n };\n } finally {\n await closeRemoteResponse(result);\n }\n }\n throw new Error('Remote URL exceeded the maximum number of redirects');\n };\n try {\n return timeout ? await Promise.race([run(), timeout]) : await run();\n } finally {\n if (deadline) clearTimeout(deadline);\n }\n}\n","import { writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport type { AIClientOptions } from '@happyvertical/ai';\nimport { fetchDocument } from '@happyvertical/documents';\nimport { ensureDirectoryExists } from '@happyvertical/files';\nimport { createLogger } from '@happyvertical/logger';\nimport type { SmrtCollectionOptions } from '@happyvertical/smrt-core';\nimport { SmrtCollection, smrt } from '@happyvertical/smrt-core';\nimport type { Image } from '@happyvertical/smrt-images';\nimport { queryGlobal, queryWithGlobals } from '@happyvertical/smrt-tenancy';\nimport type { DataQueryResult } from '@happyvertical/smrt-types';\nimport YAML from 'yaml';\nimport { htmlToMarkdown, resolveBodyFormat } from './body-format';\nimport { Content } from './content';\nimport {\n getEffectiveContentGovernanceConfig,\n loadPersistedContentGovernanceDefinitions,\n resolveEffectiveContentGovernance,\n} from './content-governance';\nimport { executeContentQuery } from './content-query';\nimport {\n type ResolveHostname,\n redactUrlCredentials,\n resolveSafeFinalUrl,\n} from './safe-remote-url';\nimport { serializeContent, serializeFact } from './serialization';\nimport type {\n ThumbnailOptions,\n ThumbnailStrategy,\n} from './thumbnail-generator';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Options accepted by {@link Contents.generateMissingThumbnails}. Also reused\n * (as a `Partial`) for the `thumbnail` config block passed through the\n * collection constructor from `smrt.config.js`.\n */\nexport interface GenerateMissingThumbnailsOptions {\n /**\n * Thumbnail generation strategy\n */\n strategy: ThumbnailStrategy;\n\n /**\n * Optional filter for content to process\n */\n where?: Record<string, unknown>;\n\n /**\n * Maximum number of thumbnails to generate\n */\n limit?: number;\n\n // Headline card options\n brandColor?: string;\n backgroundColor?: string;\n logoUrl?: string;\n template?: 'default' | 'news' | 'minimal';\n\n // Static map options\n mapProvider?: 'mapbox' | 'google';\n zoom?: number;\n\n // AI options\n style?: 'photorealistic' | 'illustration' | 'abstract' | 'minimal';\n\n // Common options\n width?: number;\n height?: number;\n}\n\n/**\n * Configuration options for Contents collection\n */\nexport interface ContentsOptions extends SmrtCollectionOptions {\n /**\n * Directory to store content files\n */\n contentDir?: string;\n\n /**\n * Default thumbnail-generation settings sourced from `smrt.config.js`. Merged\n * under per-call options in `generateMissingThumbnails`.\n */\n thumbnail?: Partial<GenerateMissingThumbnailsOptions>;\n}\n\nfunction isAIClientOptions(\n ai: SmrtCollectionOptions['ai'],\n): ai is AIClientOptions {\n return (\n !!ai &&\n typeof ai === 'object' &&\n !('embed' in ai) &&\n !('generateImage' in ai)\n );\n}\n\n/**\n * Collection for managing Content objects\n *\n * The Contents collection provides functionality for managing and manipulating\n * collections of Content objects, including saving to the filesystem and\n * mirroring content from remote URLs.\n */\n@smrt({\n api: {\n include: [\n 'browseFacts',\n 'getBySlug',\n 'getGovernanceDefinitionsAction',\n 'resolveGovernanceAction',\n 'queryAction',\n ],\n routes: {\n queryAction: {\n scope: 'collection',\n method: 'POST',\n path: 'query',\n },\n browseFacts: {\n scope: 'collection',\n method: 'GET',\n path: 'facts',\n },\n getBySlug: {\n scope: 'collection',\n method: 'GET',\n path: 'by-slug',\n },\n getGovernanceDefinitionsAction: {\n scope: 'collection',\n method: 'GET',\n path: 'governance',\n },\n resolveGovernanceAction: {\n scope: 'collection',\n method: 'GET',\n path: 'governance/resolve',\n },\n },\n },\n mcp: false,\n cli: false,\n})\nexport class Contents extends SmrtCollection<Content> { static __smrtPackage__ = \"@happyvertical/smrt-content\";\n /**\n * Class constructor for collection items\n */\n static _itemClass = Content;\n\n /**\n * Configuration options\n */\n public options: ContentsOptions = {} as ContentsOptions;\n\n /**\n * Directory to store content files\n */\n public contentDir?: string;\n\n /**\n * Cache for loaded content\n */\n public loaded: Map<string, Content>;\n\n /**\n * Creates a new Contents collection\n *\n * Use the static `create()` method inherited from SmrtCollection for proper initialization.\n *\n * @param options - Configuration options\n */\n constructor(options: ContentsOptions = {}) {\n super(options);\n this.options = options;\n this.loaded = new Map();\n }\n\n /**\n * Gets the database interface\n *\n * @returns Database interface\n */\n getDb() {\n return this._db;\n }\n\n /**\n * Initializes the collection\n *\n * @returns Promise that resolves to this instance\n */\n public async initialize(): Promise<this> {\n await super.initialize();\n return this;\n }\n\n private async getFactCollection() {\n const { FactCollection } = await import('@happyvertical/smrt-facts');\n return FactCollection.create(this.options);\n }\n\n /**\n * Bounded, tenant-safe content query (`POST /api/v1/contents/query`).\n *\n * Accepts the canonical `DataQueryRequest` envelope (#2444) and returns a\n * normalized `DataQueryResult`, so a list surface can filter, sort, page,\n * count, and facet server-side instead of hydrating the whole collection.\n *\n * The request body carries no authority: only schema-declared field ids are\n * accepted, and tenant scoping is applied by `executeContentQuery` itself\n * (fail-closed to global rows when tenancy is enabled with no active tenant\n * context). Applications that need additional site/organization scoping call\n * `executeContentQuery` directly with a trusted `scope`.\n *\n * The parameter is named `options` deliberately: the route generator passes\n * the raw request body straight through as a single `options` argument, so\n * the wire body IS the `DataQueryRequest` rather than a wrapper object.\n *\n * @param options Untrusted `DataQueryRequest` from the caller.\n * @returns A validated, bounded `DataQueryResult`.\n */\n public async queryAction(options: unknown): Promise<DataQueryResult> {\n return executeContentQuery(this, options);\n }\n\n public async browseFacts(\n options: {\n q?: string;\n query?: string;\n limit?: number | string;\n offset?: number | string;\n minSimilarity?: number | string;\n includeSuperseded?: boolean | string;\n latestOnly?: boolean | string;\n tenantId?: string | null;\n } = {},\n ) {\n try {\n const facts = await this.getFactCollection();\n const query = options.query || options.q || '';\n const limit =\n options.limit !== undefined ? Number(options.limit) : undefined;\n const offset =\n options.offset !== undefined ? Number(options.offset) : undefined;\n const minSimilarity =\n options.minSimilarity !== undefined\n ? Number(options.minSimilarity)\n : undefined;\n const includeSuperseded =\n options.includeSuperseded === true ||\n options.includeSuperseded === 'true';\n const latestOnly =\n options.latestOnly === undefined\n ? true\n : options.latestOnly === true || options.latestOnly === 'true';\n\n const results = await facts.browseCatalog(query, {\n limit: Number.isFinite(limit) ? limit : undefined,\n offset: Number.isFinite(offset) ? offset : undefined,\n minSimilarity: Number.isFinite(minSimilarity)\n ? minSimilarity\n : undefined,\n includeSuperseded,\n latestOnly,\n tenantId: options.tenantId ?? null,\n });\n\n return results.map(serializeFact);\n } catch (error) {\n // Gracefully handle missing facts table (cross-package dependency)\n if (\n typeof error === 'object' &&\n error !== null &&\n (error as { code?: unknown }).code === 'DB_SCHEMA_MISSING'\n ) {\n return [];\n }\n throw error;\n }\n }\n\n public async getBySlug(\n options: {\n slug?: string;\n context?: string;\n status?: string;\n tenantId?: string | null;\n } = {},\n ) {\n if (!options.slug) {\n throw new Error('slug is required');\n }\n\n const where: {\n slug: string;\n context: string;\n tenantId?: string | null;\n } = {\n slug: options.slug,\n context: options.context || '',\n };\n if (options.tenantId !== undefined) {\n where.tenantId = options.tenantId;\n }\n\n const content = await this.get(where);\n\n if (!content) {\n return null;\n }\n\n if (options.status && content.status !== options.status) {\n return null;\n }\n\n return serializeContent(content);\n }\n\n public async getGovernanceDefinitionsAction(\n options: { tenantId?: string | null } = {},\n ) {\n const [effective, persisted] = await Promise.all([\n getEffectiveContentGovernanceConfig({\n db: this.db,\n tenantId: options.tenantId,\n }),\n loadPersistedContentGovernanceDefinitions({\n db: this.db,\n tenantId: options.tenantId,\n }),\n ]);\n\n // Persisted rows list the global layer first, then the tenant's; keys\n // repeat across the layers (#3305 M1), so the tenant override is the\n // row an effective entry stands for.\n const effectiveRow = <T extends { key?: string }>(\n rows: T[],\n key?: string,\n ) => [...rows].reverse().find((item) => item.key === key) || {};\n\n return {\n effective: {\n policies: effective.policies.map((policy) => ({\n ...policy,\n ...effectiveRow(persisted.policies, policy.key),\n })),\n profiles: effective.profiles.map((profile) => ({\n ...profile,\n ...effectiveRow(persisted.profiles, profile.key),\n })),\n assignments: effective.assignments.map((assignment) => ({\n ...assignment,\n ...effectiveRow(persisted.assignments, assignment.key),\n })),\n },\n persisted: {\n policies: persisted.policies,\n profiles: persisted.profiles,\n assignments: persisted.assignments,\n },\n };\n }\n\n public async resolveGovernanceAction(\n options: {\n type?: string;\n variant?: string | null;\n tenantId?: string | null;\n } = {},\n ) {\n return resolveEffectiveContentGovernance({\n contentType: options.type || null,\n contentVariant: options.variant || null,\n db: this.db,\n tenantId: options.tenantId,\n });\n }\n\n /**\n * Mirrors content from a remote URL\n *\n * Downloads and stores content from a remote URL, extracting text\n * and saving it as a Content object.\n *\n * @param options - Mirror options\n * @param options.url - URL to mirror\n * @param options.mirrorDir - Directory for caching mirrored files\n * @param options.context - Context for the mirrored content\n * @returns Promise resolving to the mirrored Content object\n * @throws Error if URL is invalid or missing\n */\n public async mirror(options: {\n url: string;\n mirrorDir?: string;\n context?: string;\n /**\n * Skip the SSRF host-blocking checks. Only set this for fully trusted,\n * operator-supplied URLs (e.g. local development), never for URLs that\n * originate from end users or external content.\n */\n allowPrivateNetworkHosts?: boolean;\n /** Injectable DNS resolver (primarily for tests). */\n resolveHostname?: ResolveHostname;\n /** Injectable fetch for redirect resolution (primarily for tests). */\n fetchImpl?: typeof fetch;\n }) {\n if (!options.url) {\n throw new Error('No URL provided');\n }\n // Validate the URL AND block private/loopback/link-local/metadata hosts\n // before fetching — `mirror()` fetches an arbitrary caller-supplied URL,\n // which is a classic SSRF sink (e.g. http://169.254.169.254/ metadata).\n // `fetchDocument` follows redirects on its own, so we resolve the redirect\n // chain ourselves first — re-validating every hop — and hand it the\n // already-validated terminal URL, closing the public-host-30x-to-internal\n // bypass (S5 #1388 / review #1562).\n let url: URL;\n try {\n url = await resolveSafeFinalUrl(options.url, {\n allowPrivateNetworkHosts: options.allowPrivateNetworkHosts,\n resolveHostname: options.resolveHostname,\n fetchImpl: options.fetchImpl,\n });\n } catch (error) {\n // Never echo the raw URL — it may carry userinfo credentials (review #1562).\n const safeUrl = redactUrlCredentials(options.url);\n logger.error('Refusing to mirror unsafe URL', { error, url: safeUrl });\n throw new Error(`Invalid URL provided: ${safeUrl}`);\n }\n const existing = await this.get({ url: options.url });\n if (existing) {\n return existing;\n }\n\n // Fetch and process the document via the already-validated terminal URL.\n const doc = await fetchDocument(url.toString(), {\n cacheDir: options?.mirrorDir,\n });\n\n const filename = url.pathname.split('/').pop();\n const nameWithoutExtension = filename?.replace(/\\.[^/.]+$/, '');\n const title = nameWithoutExtension?.replace(/[-_]/g, ' ');\n\n // Extract text from all document parts\n const body = doc.parts.map((part) => part.content).join('\\n\\n');\n if (body) {\n const content = new Content({\n url: options.url,\n type: 'mirror',\n title,\n context: options.context || '',\n body,\n });\n await content.initialize();\n // The file name is not an identity (two sites' `index.html`), so the\n // slug is derived, not set: a taken one moves to `-2` on save instead\n // of overwriting the other mirror (the URL lookup above is the dedup).\n await content.getSlug();\n await content.save();\n return content;\n }\n }\n\n /**\n * Writes a Content object to the filesystem as a markdown file\n *\n * @param options - Options for writing the content file\n * @param options.content - Content object to write\n * @param options.contentDir - Directory to write the file to\n * @returns Promise that resolves when the file is written\n * @throws Error if contentDir is not provided\n */\n public async writeContentFile(options: {\n content: Content;\n contentDir: string;\n }) {\n const { content, contentDir } = options;\n if (!contentDir) {\n throw new Error('No content dir provided');\n }\n\n const { body } = content;\n const frontMatter = {\n title: content.title,\n slug: content.slug,\n context: content.context,\n author: content.author,\n publish_date: content.publish_date,\n };\n\n let output = '';\n if (frontMatter && Object.keys(frontMatter).length > 0) {\n output += '---\\n';\n output += YAML.stringify(frontMatter);\n output += '---\\n';\n }\n\n // Filesystem exports are markdown regardless of the editor save format.\n let formattedBody = body || '';\n const bodyFormat = resolveBodyFormat(content.bodyFormat, body);\n if (bodyFormat === 'html') {\n formattedBody = htmlToMarkdown(body || '');\n } else if (body && !this.isMarkdown(body)) {\n formattedBody = this.formatAsMarkdown(body);\n }\n output += formattedBody;\n\n const pathParts = [\n contentDir,\n content.context || '', // if empty, use empty string\n content.slug,\n 'index.md',\n ].filter(Boolean); // remove empty strings\n\n const outputFile = path.join(...(pathParts as string[]));\n\n // `context` and `slug` are persisted, caller-influenced fields. Without a\n // guard, a value like `../../etc/cron.d/x` escapes `contentDir` and lets an\n // export overwrite arbitrary files (path traversal). Confirm the joined\n // path still lives under the resolved content directory (S5 #1388).\n const resolvedDir = path.resolve(contentDir);\n const resolvedFile = path.resolve(outputFile);\n if (\n resolvedFile !== resolvedDir &&\n !resolvedFile.startsWith(resolvedDir + path.sep)\n ) {\n throw new Error(\n 'Refusing to write content file outside of the content directory',\n );\n }\n\n await ensureDirectoryExists(path.dirname(outputFile));\n await writeFile(outputFile, output);\n }\n\n /**\n * Checks if text appears to be in markdown format\n *\n * @param text - Text to check\n * @returns Boolean indicating if the text contains markdown syntax\n */\n private isMarkdown(text: string): boolean {\n // Basic check for common markdown indicators\n const markdownIndicators = [\n /^#\\s/m, // Headers\n /\\*\\*.+\\*\\*/, // Bold\n /\\*.+\\*/, // Italic\n /\\[.+\\]\\(.+\\)/, // Links\n /^\\s*[-*+]\\s/m, // Lists\n /^\\s*\\d+\\.\\s/m, // Numbered lists\n /```[\\s\\S]*```/, // Code blocks\n /^\\s*>/m, // Blockquotes\n ];\n\n return markdownIndicators.some((indicator) => indicator.test(text));\n }\n\n /**\n * Formats plain text as simple markdown\n *\n * @param text - Plain text to format\n * @returns Text formatted as basic markdown\n */\n private formatAsMarkdown(text: string): string {\n // Basic formatting of plain text to markdown\n return text\n .split(/\\n\\n+/)\n .map((paragraph) => paragraph.trim())\n .filter(Boolean)\n .join('\\n\\n');\n }\n\n /**\n * Synchronizes content to the filesystem\n *\n * Writes all article-type Content objects to the filesystem\n * as markdown files.\n *\n * @param options - Sync options\n * @param options.contentDir - Directory to write content files to\n * @returns Promise that resolves when synchronization is complete\n */\n public async syncContentDir(options: { contentDir?: string }) {\n const contentFilter = {\n type: 'article',\n };\n\n const contents = await this.list({ where: contentFilter });\n for (const content of contents) {\n await this.writeContentFile({\n content,\n contentDir: options.contentDir || this.options.contentDir || '',\n });\n }\n }\n\n /**\n * Generate thumbnails for content that doesn't have one\n *\n * @param options - Options for bulk thumbnail generation\n * @returns Promise resolving to result object with generated images and failed content IDs\n *\n * @example Generate headline cards for all published articles\n * ```typescript\n * const result = await contents.generateMissingThumbnails({\n * strategy: 'headline-card',\n * where: { type: 'article', status: 'published' },\n * brandColor: '#1a56db'\n * });\n * console.log(`Generated ${result.images.length} thumbnails`);\n * if (result.failed.length > 0) {\n * console.warn(`Failed to generate ${result.failed.length} thumbnails`);\n * }\n * ```\n */\n public async generateMissingThumbnails(\n options: GenerateMissingThumbnailsOptions,\n ): Promise<{\n images: Image[];\n failed: Array<{ contentId: string; error: string }>;\n }> {\n // Merge with thumbnail config from smrt.config.js (passed via constructor)\n // This allows CLI users to configure defaults in smrt.config.js:\n // thumbnail: { strategy: 'headline-card', brandColor: '#1976d2' }\n const configDefaults = this.options?.thumbnail || {};\n const mergedOptions = {\n ...configDefaults,\n ...options,\n };\n\n // Build query for content missing thumbnails\n const whereClause = {\n ...mergedOptions.where,\n thumbnailAssetId: null,\n };\n\n const contents = await this.list({\n where: whereClause,\n limit: mergedOptions.limit,\n });\n\n const generatedImages: Image[] = [];\n const failed: Array<{ contentId: string; error: string }> = [];\n\n for (const content of contents) {\n try {\n // Build thumbnail options based on strategy\n let thumbnailOptions: ThumbnailOptions;\n\n switch (mergedOptions.strategy) {\n case 'headline-card':\n thumbnailOptions = {\n strategy: 'headline-card',\n brandColor: mergedOptions.brandColor,\n backgroundColor: mergedOptions.backgroundColor,\n logoUrl: mergedOptions.logoUrl,\n template: mergedOptions.template,\n width: mergedOptions.width,\n height: mergedOptions.height,\n };\n break;\n\n case 'static-map':\n thumbnailOptions = {\n strategy: 'static-map',\n mapProvider: mergedOptions.mapProvider,\n zoom: mergedOptions.zoom,\n width: mergedOptions.width,\n height: mergedOptions.height,\n };\n break;\n\n case 'ai-generate':\n thumbnailOptions = {\n strategy: 'ai-generate',\n style: mergedOptions.style,\n width: mergedOptions.width,\n height: mergedOptions.height,\n ai: isAIClientOptions(this.options.ai)\n ? this.options.ai\n : undefined,\n };\n break;\n\n default:\n throw new Error(`Unknown strategy: ${mergedOptions.strategy}`);\n }\n\n const image = await content.generateThumbnail(thumbnailOptions);\n generatedImages.push(image);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n logger.error(\n `Failed to generate thumbnail for content ${content.id}: ${errorMessage}`,\n );\n failed.push({\n contentId: content.id ?? 'unknown',\n error: errorMessage,\n });\n }\n }\n\n return { images: generatedImages, failed };\n }\n\n // ============================================\n // Tenant Helper Methods\n // ============================================\n\n /**\n * Find all content belonging to a specific tenant\n *\n * @param tenantId - The tenant ID to filter by\n * @returns Promise resolving to array of Content objects for the tenant\n *\n * @example\n * ```typescript\n * const tenantContent = await contents.findByTenant('tenant-123');\n * ```\n */\n async findByTenant(tenantId: string): Promise<Content[]> {\n return this.list({ where: { tenantId } });\n }\n\n /**\n * Find all global content (not associated with any tenant).\n *\n * Routes through the shared tenant-global helper so it does not throw under\n * an active tenant context (an explicit `tenant_id IS NULL` filter would be\n * flagged as an isolation violation). (#1600)\n *\n * @returns Promise resolving to array of global Content objects\n *\n * @example\n * ```typescript\n * const globalContent = await contents.findGlobal();\n * ```\n */\n async findGlobal(): Promise<Content[]> {\n return queryGlobal<Content>(this);\n }\n\n /**\n * Find content for a tenant including global content.\n *\n * This returns both tenant-specific content and global content (tenantId is null),\n * useful for showing a tenant their content plus any shared/global resources.\n *\n * Fails closed if an active tenant context requests a different tenant's\n * rows; the admin/system path keeps the cross-tenant capability. (#1600)\n *\n * @param tenantId - The tenant ID to include\n * @returns Promise resolving to array of Content objects (tenant + global)\n *\n * @example\n * ```typescript\n * const allAccessibleContent = await contents.findWithGlobals('tenant-123');\n * ```\n */\n async findWithGlobals(tenantId: string): Promise<Content[]> {\n return queryWithGlobals<Content>(this, tenantId, 'Content.findWithGlobals');\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"contents-DDiybAlF.js","names":["body"],"sources":["../../src/safe-remote-url.ts","../../src/contents.ts"],"sourcesContent":["/**\n * Shared SSRF guard for content's outbound fetches (feed sync, mirroring).\n *\n * Any code path that fetches a caller-supplied URL — RSS/Atom feeds, the\n * `Mirror` content type, link previews — is an SSRF vector: an attacker who can\n * influence the URL can make the server fetch `169.254.169.254` cloud metadata,\n * `localhost` admin panels, or other internal services. This module rejects\n * URLs that resolve to private, loopback, link-local, CGNAT, or cloud-metadata\n * ranges before any request is made, and re-validates redirect hops.\n *\n * Unlike a literal-IP-only check, {@link assertSafeRemoteUrl} resolves hostnames\n * via DNS so a public name pointing at a private IP is also caught (S5 #1388).\n */\nimport { lookup as dnsLookup } from 'node:dns/promises';\nimport { isIP } from 'node:net';\nimport { Agent, fetch as undiciFetch } from 'undici';\n\nexport type ResolvedAddress = { address: string; family?: number };\nexport type ResolveHostname = (hostname: string) => Promise<ResolvedAddress[]>;\n\nexport interface SafeRemoteUrlOptions {\n /** Skip the private-network checks (trusted callers only, e.g. local dev). */\n allowPrivateNetworkHosts?: boolean;\n /** Injectable resolver for tests; defaults to {@link defaultResolveHostname}. */\n resolveHostname?: ResolveHostname;\n}\n\nexport interface ValidatedRemoteUrlOptions extends SafeRemoteUrlOptions {\n /** Resolve even when private hosts are explicitly allowed, for pinned fetches. */\n resolveHostnameWhenPrivateAllowed?: boolean;\n}\n\n/** A URL together with the exact addresses accepted by its DNS validation. */\nexport interface ValidatedRemoteUrl {\n url: URL;\n addresses: ResolvedAddress[];\n}\n\n/** Options for bounded, SSRF-safe HTTPS retrieval. */\nexport interface SafeRemoteFetchOptions {\n /** Only Accept, Accept-Language, If-Modified-Since, If-None-Match, and User-Agent. */\n headers?: Record<string, string>;\n /** Total DNS, request, redirect, and response-body deadline. Defaults to 10s. */\n timeoutMs?: number;\n /** Maximum returned response-body bytes. Defaults to 2 MB (2,000,000 bytes). */\n maxBytes?: number;\n /** Maximum validated redirect hops. Defaults to 5. Set 0 to reject redirects. */\n maxRedirects?: number;\n /** Injectable DNS resolver. Its results are still validated and pinned. */\n resolveHostname?: ResolveHostname;\n}\n\n/** A fully buffered response from {@link fetchSafeRemoteUrl}. */\nexport interface SafeRemoteFetchResult {\n /** Final URL after validated redirects. */\n url: URL;\n status: number;\n statusText: string;\n ok: boolean;\n /** Lower-cased response header names. */\n headers: Readonly<Record<string, string>>;\n /** Bounded response bytes. */\n body: Uint8Array;\n}\n\nexport async function defaultResolveHostname(\n hostname: string,\n): Promise<ResolvedAddress[]> {\n return dnsLookup(hostname, { all: true, verbatim: false });\n}\n\nexport function isBlockedIPv4(address: string): boolean {\n const parts = address.split('.');\n if (parts.length !== 4) return true;\n // Strict per-octet validation: `Number('')` is 0, so without a digit check a\n // malformed input like `1..2.3` would parse to `[1,0,2,3]` and be treated as a\n // valid (and possibly allowed) address. Anything not 1-3 digits in 0-255 is\n // unparseable → blocked (review #1562).\n const octets = parts.map((part) =>\n /^\\d{1,3}$/.test(part) ? Number(part) : Number.NaN,\n );\n if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {\n return true;\n }\n\n const [first, second, third] = octets;\n return (\n first === 0 ||\n first === 10 ||\n first === 127 ||\n first >= 224 ||\n (first === 100 && second >= 64 && second <= 127) ||\n (first === 169 && second === 254) ||\n (first === 172 && second >= 16 && second <= 31) ||\n (first === 192 && second === 168) ||\n (first === 192 && second === 0 && (third === 0 || third === 2)) ||\n (first === 192 && second === 88 && third === 99) ||\n (first === 198 && (second === 18 || second === 19)) ||\n (first === 198 && second === 51 && third === 100) ||\n (first === 203 && second === 0 && third === 113)\n );\n}\n\n/**\n * Expand an IPv6 string to its 8 hextets (numbers), or null if unparseable.\n * Handles `::` compression, an embedded dotted-IPv4 tail, and bracketed forms.\n */\nfunction expandIPv6(address: string): number[] | null {\n let work = address.toLowerCase().replace(/^\\[|\\]$/g, '');\n if (work.includes('.')) {\n // Embedded dotted IPv4 tail (e.g. ::ffff:127.0.0.1) → convert to 2 hextets.\n const m = work.match(/(\\d{1,3}(?:\\.\\d{1,3}){3})$/);\n if (!m) return null;\n const o = m[1].split('.').map(Number);\n if (o.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;\n const hi = ((o[0] << 8) | o[1]).toString(16);\n const lo = ((o[2] << 8) | o[3]).toString(16);\n work = `${work.slice(0, work.length - m[1].length)}${hi}:${lo}`;\n }\n const halves = work.split('::');\n if (halves.length > 2) return null;\n const head = halves[0] ? halves[0].split(':') : [];\n const tail = halves.length === 2 && halves[1] ? halves[1].split(':') : [];\n const groups =\n halves.length === 2\n ? [\n ...head,\n ...Array(Math.max(0, 8 - head.length - tail.length)).fill('0'),\n ...tail,\n ]\n : head;\n if (groups.length !== 8) return null;\n const hextets = groups.map((g) =>\n /^[0-9a-f]{1,4}$/.test(g) ? Number.parseInt(g, 16) : Number.NaN,\n );\n return hextets.some((n) => Number.isNaN(n)) ? null : hextets;\n}\n\nexport function isBlockedIPv6(address: string): boolean {\n const hextets = expandIPv6(address);\n if (hextets) {\n // IPv4-mapped (::ffff:a.b.c.d) addresses embed an IPv4 in the last two\n // hextets — decode and apply the IPv4\n // blocklist so a loopback/private IPv4 can't be smuggled through any IPv6\n // encoding (compressed, expanded, dotted, or hex) (review #1562, P1).\n const firstFiveZero = hextets.slice(0, 5).every((h) => h === 0);\n if (firstFiveZero && hextets[5] === 0xffff) {\n const [, , , , , , g6, g7] = hextets;\n const ipv4 = `${g6 >> 8}.${g6 & 0xff}.${g7 >> 8}.${g7 & 0xff}`;\n return isBlockedIPv4(ipv4);\n }\n }\n\n // Conservatively allow global unicast only, excluding IETF special-purpose,\n // documentation, and transition ranges. Compare parsed hextets so expanded\n // spellings cannot bypass the guard. IPv4-mapped addresses are checked above.\n // https://www.iana.org/assignments/iana-ipv6-special-registry/\n if (!hextets) return true;\n return (\n (hextets[0] & 0xe000) !== 0x2000 ||\n (hextets[0] === 0x2001 && hextets[1] < 0x0200) ||\n (hextets[0] === 0x2001 && hextets[1] === 0x0db8) ||\n hextets[0] === 0x2002 ||\n (hextets[0] === 0x3fff && hextets[1] < 0x1000)\n );\n}\n\nexport function isBlockedAddress(address: string): boolean {\n const family = isIP(address);\n if (family === 4) return isBlockedIPv4(address);\n if (family === 6) return isBlockedIPv6(address);\n // Anything that isn't a recognisable IP literal (after DNS resolution should\n // have produced one) is treated as unsafe.\n return true;\n}\n\n/**\n * Parse and validate a remote URL for outbound fetching. Rejects non-http(s)\n * schemes, embedded credentials, and hosts that resolve to non-public ranges.\n * Returns the parsed URL and the exact accepted addresses so callers can bind\n * their connection without resolving the hostname again.\n */\nexport async function validateSafeRemoteUrl(\n rawUrl: string,\n options: ValidatedRemoteUrlOptions = {},\n): Promise<ValidatedRemoteUrl> {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new Error('Remote URL must be an absolute URL');\n }\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new Error('Remote URL must use http or https');\n }\n if (url.username || url.password) {\n throw new Error('Remote URL must not include credentials');\n }\n if (!url.hostname) {\n throw new Error('Remote URL must include a hostname');\n }\n\n if (\n options.allowPrivateNetworkHosts &&\n !options.resolveHostnameWhenPrivateAllowed\n ) {\n return { url, addresses: [] };\n }\n\n const resolver = options.resolveHostname ?? defaultResolveHostname;\n const addresses =\n isIP(url.hostname) === 0\n ? await resolver(url.hostname)\n : [{ address: url.hostname }];\n\n if (\n !addresses.length ||\n (!options.allowPrivateNetworkHosts &&\n addresses.some(({ address }) => isBlockedAddress(address)))\n ) {\n throw new Error('Remote URL must resolve to a public network address');\n }\n\n return { url, addresses };\n}\n\nexport async function assertSafeRemoteUrl(\n rawUrl: string,\n options: SafeRemoteUrlOptions = {},\n): Promise<URL> {\n return (await validateSafeRemoteUrl(rawUrl, options)).url;\n}\n\n/** Redirect status codes that reroute a request to a new Location. */\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\nconst DEFAULT_MAX_REDIRECTS = 5;\nconst DEFAULT_RESOLVE_TIMEOUT_MS = 10_000;\n\nexport interface SafeRedirectOptions extends SafeRemoteUrlOptions {\n /** Maximum redirect hops to follow before failing. Default 5. */\n maxRedirects?: number;\n /** Per-hop timeout in ms. Default 10s. */\n timeoutMs?: number;\n /** Injectable fetch (primarily for tests). Defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Resolve a URL's redirect chain and return the final safe {@link URL}, with\n * EVERY hop re-validated through {@link assertSafeRemoteUrl}.\n *\n * Use this before handing a URL to a fetcher that follows redirects on its own\n * (e.g. `fetchDocument`): the up-front {@link assertSafeRemoteUrl} check alone\n * can't stop an allowed public host from `30x`-redirecting into an internal /\n * loopback / metadata host, which would defeat the SSRF guard (review #1562).\n *\n * Redirects are followed with `GET` + `redirect: 'manual'` to match downstream\n * GET-based fetchers; response bodies are discarded (the redirect bodies are\n * empty and the terminal body is left for the caller to re-fetch), so this does\n * not double-download content.\n */\nexport async function resolveSafeFinalUrl(\n rawUrl: string,\n options: SafeRedirectOptions = {},\n): Promise<URL> {\n const fetchImpl = options.fetchImpl ?? fetch;\n const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;\n const timeoutMs = options.timeoutMs ?? DEFAULT_RESOLVE_TIMEOUT_MS;\n let current = await assertSafeRemoteUrl(rawUrl, options);\n\n for (let hop = 0; hop <= maxRedirects; hop += 1) {\n const response = await fetchImpl(current, {\n method: 'GET',\n redirect: 'manual',\n signal: AbortSignal.timeout(timeoutMs),\n });\n // Release the connection without downloading the body.\n try {\n await response.body?.cancel();\n } catch {\n // best-effort\n }\n\n if (!REDIRECT_STATUSES.has(response.status)) {\n return current;\n }\n const location = response.headers.get('location');\n if (!location) return current;\n // Re-validate the resolved redirect target before following it.\n current = await assertSafeRemoteUrl(\n new URL(location, current).toString(),\n options,\n );\n }\n\n throw new Error('Remote URL exceeded the maximum number of redirects');\n}\n\n/**\n * Strip userinfo (`user:pass@`) from a URL so it can be safely logged or echoed\n * in an error message. Returns a placeholder for unparseable input. Never let a\n * credential-bearing URL reach logs/errors verbatim (review #1562).\n */\nexport function redactUrlCredentials(raw: string): string {\n try {\n const url = new URL(raw);\n if (url.username || url.password) {\n url.username = '';\n url.password = '';\n return url.toString();\n }\n return raw;\n } catch {\n return '[unparseable url]';\n }\n}\n\nconst DEFAULT_MAX_RESPONSE_BYTES = 2_000_000;\nconst DEFAULT_FETCH_TIMEOUT_MS = 10_000;\nconst SAFE_REQUEST_HEADER_NAMES = new Set([\n 'accept',\n 'accept-language',\n 'if-modified-since',\n 'if-none-match',\n 'user-agent',\n]);\n\nfunction assertSafeRequestHeaders(headers: Record<string, string> | undefined) {\n for (const name of Object.keys(headers ?? {})) {\n if (!SAFE_REQUEST_HEADER_NAMES.has(name.toLowerCase())) {\n throw new Error(\n `Safe remote fetch does not allow request header ${name}`,\n );\n }\n }\n}\n\ntype RemoteResponse = {\n status: number;\n statusText: string;\n ok: boolean;\n headers: {\n get: (name: string) => string | null;\n forEach: (callback: (value: string, key: string) => void) => void;\n };\n body: {\n locked: boolean;\n cancel: (reason?: unknown) => Promise<void>;\n getReader: () => {\n cancel: (reason?: unknown) => Promise<void>;\n read: () => Promise<{ done: boolean; value?: Uint8Array }>;\n releaseLock: () => void;\n };\n } | null;\n arrayBuffer: () => Promise<ArrayBuffer>;\n};\n\ntype RemoteFetchResult = {\n response: RemoteResponse;\n close: () => Promise<void>;\n};\n\ninterface InternalSafeRemoteFetchOptions extends SafeRemoteFetchOptions {\n allowHttp?: boolean;\n allowPrivateNetworkHosts?: boolean;\n fetchImpl?: typeof fetch;\n}\n\nfunction createPinnedDispatcher(addresses: ResolvedAddress[]): Agent {\n const pinnedAddresses = addresses.map(({ address, family }) => {\n const resolvedFamily = family ?? isIP(address);\n if (resolvedFamily !== 4 && resolvedFamily !== 6) {\n throw new Error('Remote URL resolved to an unrecognised network address');\n }\n return { address, family: resolvedFamily };\n });\n\n return new Agent({\n connect: {\n lookup: (_hostname, lookupOptions, callback) => {\n if (lookupOptions.all) {\n callback(null, pinnedAddresses);\n return;\n }\n const selected =\n pinnedAddresses.find(\n ({ family }) =>\n !lookupOptions.family || family === lookupOptions.family,\n ) ?? pinnedAddresses[0];\n callback(null, selected.address, selected.family);\n },\n },\n });\n}\n\nasync function fetchPinnedRemoteUrl(\n url: ValidatedRemoteUrl,\n options: InternalSafeRemoteFetchOptions,\n signal: AbortSignal | undefined,\n): Promise<RemoteFetchResult> {\n const init = {\n headers: options.headers,\n redirect: 'manual' as const,\n signal,\n };\n // This is an internal trusted test/embedding seam. Public callers use the\n // Undici transport below, which cannot resolve the hostname a second time.\n if (options.fetchImpl) {\n return {\n response: await options.fetchImpl(url.url, init),\n close: async () => {},\n };\n }\n const dispatcher = createPinnedDispatcher(url.addresses);\n try {\n return {\n response: await undiciFetch(url.url, { ...init, dispatcher }),\n close: () => dispatcher.close(),\n };\n } catch (error) {\n await dispatcher.close();\n throw error;\n }\n}\n\nasync function closeRemoteResponse({ response, close }: RemoteFetchResult) {\n try {\n if (response.body && !response.body.locked) await response.body.cancel();\n } catch {\n // A failed stream may reject cancellation; the dispatcher still closes.\n } finally {\n await close();\n }\n}\n\nasync function readBoundedResponseBytes(\n response: RemoteResponse,\n maxBytes: number,\n): Promise<Uint8Array> {\n // A 304 has no message body; its Content-Length describes the selected\n // representation, not bytes transferred in this response.\n if (response.status === 304) return new Uint8Array();\n const contentLength = response.headers.get('content-length');\n if (contentLength && Number(contentLength) > maxBytes) {\n throw new Error(`Remote response exceeds ${maxBytes} bytes`);\n }\n if (!response.body) {\n const body = new Uint8Array(await response.arrayBuffer());\n if (body.byteLength > maxBytes)\n throw new Error(`Remote response exceeds ${maxBytes} bytes`);\n return body;\n }\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (!value) continue;\n totalBytes += value.byteLength;\n if (totalBytes > maxBytes) {\n await reader.cancel();\n throw new Error(`Remote response exceeds ${maxBytes} bytes`);\n }\n chunks.push(value);\n }\n } catch (error) {\n await reader.cancel(error);\n throw error;\n } finally {\n reader.releaseLock();\n }\n const body = new Uint8Array(totalBytes);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return body;\n}\n\nfunction copyResponseHeaders(\n headers: RemoteResponse['headers'],\n): Readonly<Record<string, string>> {\n const result: Record<string, string> = {};\n headers.forEach((value, key) => {\n result[key.toLowerCase()] = value;\n });\n return Object.freeze(result);\n}\n\nfunction removeCrossOriginSensitiveHeaders(\n headers: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n if (!headers) return undefined;\n const safeHeaders = Object.fromEntries(\n Object.entries(headers).filter(\n ([name]) =>\n !['authorization', 'cookie', 'proxy-authorization'].includes(\n name.toLowerCase(),\n ),\n ),\n );\n return safeHeaders;\n}\n\nfunction assertFetchLimits(options: InternalSafeRemoteFetchOptions) {\n const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;\n if (!Number.isFinite(timeoutMs) || timeoutMs < 0)\n throw new Error(\n 'Remote fetch timeout must be a non-negative finite number',\n );\n if (!Number.isInteger(maxBytes) || maxBytes < 0)\n throw new Error('Remote fetch maxBytes must be a non-negative integer');\n if (!Number.isInteger(maxRedirects) || maxRedirects < 0)\n throw new Error('Remote fetch maxRedirects must be a non-negative integer');\n return { timeoutMs, maxBytes, maxRedirects };\n}\n\n/**\n * Fetch an untrusted remote HTTPS URL through a DNS-pinned transport.\n * Redirect targets are independently validated, and returned bodies are fully\n * buffered within `maxBytes`; callers never receive a live response stream.\n */\nexport async function fetchSafeRemoteUrl(\n rawUrl: string,\n options: SafeRemoteFetchOptions = {},\n): Promise<SafeRemoteFetchResult> {\n if (options.timeoutMs === 0) {\n throw new Error('Safe remote fetch timeout must be greater than zero');\n }\n assertSafeRequestHeaders(options.headers);\n return fetchSafeRemoteUrlInternal(rawUrl, options);\n}\n\n/** @internal Shared feed transport; its HTTP/private/test seams are not exported from the package barrel. */\nexport async function fetchSafeRemoteUrlInternal(\n rawUrl: string,\n options: InternalSafeRemoteFetchOptions = {},\n): Promise<SafeRemoteFetchResult> {\n const { timeoutMs, maxBytes, maxRedirects } = assertFetchLimits(options);\n const controller = timeoutMs > 0 ? new AbortController() : undefined;\n let deadline: ReturnType<typeof setTimeout> | undefined;\n const timeout =\n timeoutMs > 0\n ? new Promise<never>((_resolve, reject) => {\n deadline = setTimeout(() => {\n controller?.abort();\n reject(new Error(`Remote fetch timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n })\n : undefined;\n const run = async (): Promise<SafeRemoteFetchResult> => {\n let current = await validateSafeRemoteUrl(rawUrl, {\n allowPrivateNetworkHosts: options.allowPrivateNetworkHosts,\n resolveHostnameWhenPrivateAllowed: true,\n resolveHostname: options.resolveHostname,\n });\n let requestHeaders = options.headers;\n if (!options.allowHttp && current.url.protocol !== 'https:') {\n throw new Error('Remote fetch URL must use https');\n }\n for (let hop = 0; hop <= maxRedirects; hop += 1) {\n const result = await fetchPinnedRemoteUrl(\n current,\n {\n ...options,\n headers: requestHeaders,\n },\n controller?.signal,\n );\n const { response } = result;\n if (REDIRECT_STATUSES.has(response.status)) {\n const location = response.headers.get('location');\n if (!location) {\n await closeRemoteResponse(result);\n throw new Error('Remote redirect response missing Location header');\n }\n try {\n const previousOrigin = current.url.origin;\n current = await validateSafeRemoteUrl(\n new URL(location, current.url).toString(),\n {\n allowPrivateNetworkHosts: options.allowPrivateNetworkHosts,\n resolveHostnameWhenPrivateAllowed: true,\n resolveHostname: options.resolveHostname,\n },\n );\n if (!options.allowHttp && current.url.protocol !== 'https:') {\n throw new Error('Remote fetch URL must use https');\n }\n if (current.url.origin !== previousOrigin) {\n requestHeaders = removeCrossOriginSensitiveHeaders(requestHeaders);\n }\n } finally {\n await closeRemoteResponse(result);\n }\n continue;\n }\n try {\n return {\n url: current.url,\n status: response.status,\n statusText: response.statusText,\n ok: response.ok,\n headers: copyResponseHeaders(response.headers),\n body: await readBoundedResponseBytes(response, maxBytes),\n };\n } finally {\n await closeRemoteResponse(result);\n }\n }\n throw new Error('Remote URL exceeded the maximum number of redirects');\n };\n try {\n return timeout ? await Promise.race([run(), timeout]) : await run();\n } finally {\n if (deadline) clearTimeout(deadline);\n }\n}\n","import { writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport type { AIClientOptions } from '@happyvertical/ai';\nimport { fetchDocument } from '@happyvertical/documents';\nimport { ensureDirectoryExists } from '@happyvertical/files';\nimport { createLogger } from '@happyvertical/logger';\nimport type { SmrtCollectionOptions } from '@happyvertical/smrt-core';\nimport { SmrtCollection, smrt } from '@happyvertical/smrt-core';\nimport type { Image } from '@happyvertical/smrt-images';\nimport { queryGlobal, queryWithGlobals } from '@happyvertical/smrt-tenancy';\nimport type { DataQueryResult } from '@happyvertical/smrt-types';\nimport YAML from 'yaml';\nimport { htmlToMarkdown, resolveBodyFormat } from './body-format';\nimport { Content } from './content';\nimport {\n getEffectiveContentGovernanceConfig,\n loadPersistedContentGovernanceDefinitions,\n resolveEffectiveContentGovernance,\n} from './content-governance';\nimport { executeContentQuery } from './content-query';\nimport {\n type ResolveHostname,\n redactUrlCredentials,\n resolveSafeFinalUrl,\n} from './safe-remote-url';\nimport { serializeContent, serializeFact } from './serialization';\nimport type {\n ThumbnailOptions,\n ThumbnailStrategy,\n} from './thumbnail-generator';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Options accepted by {@link Contents.generateMissingThumbnails}. Also reused\n * (as a `Partial`) for the `thumbnail` config block passed through the\n * collection constructor from `smrt.config.js`.\n */\nexport interface GenerateMissingThumbnailsOptions {\n /**\n * Thumbnail generation strategy\n */\n strategy: ThumbnailStrategy;\n\n /**\n * Optional filter for content to process\n */\n where?: Record<string, unknown>;\n\n /**\n * Maximum number of thumbnails to generate\n */\n limit?: number;\n\n // Headline card options\n brandColor?: string;\n backgroundColor?: string;\n logoUrl?: string;\n template?: 'default' | 'news' | 'minimal';\n\n // Static map options\n mapProvider?: 'mapbox' | 'google';\n zoom?: number;\n\n // AI options\n style?: 'photorealistic' | 'illustration' | 'abstract' | 'minimal';\n\n // Common options\n width?: number;\n height?: number;\n}\n\n/**\n * Configuration options for Contents collection\n */\nexport interface ContentsOptions extends SmrtCollectionOptions {\n /**\n * Directory to store content files\n */\n contentDir?: string;\n\n /**\n * Default thumbnail-generation settings sourced from `smrt.config.js`. Merged\n * under per-call options in `generateMissingThumbnails`.\n */\n thumbnail?: Partial<GenerateMissingThumbnailsOptions>;\n}\n\nfunction isAIClientOptions(\n ai: SmrtCollectionOptions['ai'],\n): ai is AIClientOptions {\n return (\n !!ai &&\n typeof ai === 'object' &&\n !('embed' in ai) &&\n !('generateImage' in ai)\n );\n}\n\n/**\n * Collection for managing Content objects\n *\n * The Contents collection provides functionality for managing and manipulating\n * collections of Content objects, including saving to the filesystem and\n * mirroring content from remote URLs.\n */\n@smrt({\n api: {\n include: [\n 'browseFacts',\n 'getBySlug',\n 'getGovernanceDefinitionsAction',\n 'resolveGovernanceAction',\n 'queryAction',\n ],\n routes: {\n queryAction: {\n scope: 'collection',\n method: 'POST',\n path: 'query',\n },\n browseFacts: {\n scope: 'collection',\n method: 'GET',\n path: 'facts',\n },\n getBySlug: {\n scope: 'collection',\n method: 'GET',\n path: 'by-slug',\n },\n getGovernanceDefinitionsAction: {\n scope: 'collection',\n method: 'GET',\n path: 'governance',\n },\n resolveGovernanceAction: {\n scope: 'collection',\n method: 'GET',\n path: 'governance/resolve',\n },\n },\n },\n mcp: false,\n cli: false,\n})\nexport class Contents extends SmrtCollection<Content> { static __smrtPackage__ = \"@happyvertical/smrt-content\";\n /**\n * Class constructor for collection items\n */\n static _itemClass = Content;\n\n /**\n * Configuration options\n */\n public options: ContentsOptions = {} as ContentsOptions;\n\n /**\n * Directory to store content files\n */\n public contentDir?: string;\n\n /**\n * Cache for loaded content\n */\n public loaded: Map<string, Content>;\n\n /**\n * Creates a new Contents collection\n *\n * Use the static `create()` method inherited from SmrtCollection for proper initialization.\n *\n * @param options - Configuration options\n */\n constructor(options: ContentsOptions = {}) {\n super(options);\n this.options = options;\n this.loaded = new Map();\n }\n\n /**\n * Gets the database interface\n *\n * @returns Database interface\n */\n getDb() {\n return this._db;\n }\n\n /**\n * Initializes the collection\n *\n * @returns Promise that resolves to this instance\n */\n public async initialize(): Promise<this> {\n await super.initialize();\n return this;\n }\n\n private async getFactCollection() {\n const { FactCollection } = await import('@happyvertical/smrt-facts');\n return FactCollection.create(this.options);\n }\n\n /**\n * Bounded, tenant-safe content query (`POST /api/v1/contents/query`).\n *\n * Accepts the canonical `DataQueryRequest` envelope (#2444) and returns a\n * normalized `DataQueryResult`, so a list surface can filter, sort, page,\n * count, and facet server-side instead of hydrating the whole collection.\n *\n * The request body carries no authority: only schema-declared field ids are\n * accepted, and tenant scoping is applied by `executeContentQuery` itself\n * (fail-closed to global rows when tenancy is enabled with no active tenant\n * context). Applications that need additional site/organization scoping call\n * `executeContentQuery` directly with a trusted `scope`.\n *\n * The parameter is named `options` deliberately: the route generator passes\n * the raw request body straight through as a single `options` argument, so\n * the wire body IS the `DataQueryRequest` rather than a wrapper object.\n *\n * @param options Untrusted `DataQueryRequest` from the caller.\n * @returns A validated, bounded `DataQueryResult`.\n */\n public async queryAction(options: unknown): Promise<DataQueryResult> {\n return executeContentQuery(this, options);\n }\n\n public async browseFacts(\n options: {\n q?: string;\n query?: string;\n limit?: number | string;\n offset?: number | string;\n minSimilarity?: number | string;\n includeSuperseded?: boolean | string;\n latestOnly?: boolean | string;\n tenantId?: string | null;\n } = {},\n ) {\n try {\n const facts = await this.getFactCollection();\n const query = options.query || options.q || '';\n const limit =\n options.limit !== undefined ? Number(options.limit) : undefined;\n const offset =\n options.offset !== undefined ? Number(options.offset) : undefined;\n const minSimilarity =\n options.minSimilarity !== undefined\n ? Number(options.minSimilarity)\n : undefined;\n const includeSuperseded =\n options.includeSuperseded === true ||\n options.includeSuperseded === 'true';\n const latestOnly =\n options.latestOnly === undefined\n ? true\n : options.latestOnly === true || options.latestOnly === 'true';\n\n const results = await facts.browseCatalog(query, {\n limit: Number.isFinite(limit) ? limit : undefined,\n offset: Number.isFinite(offset) ? offset : undefined,\n minSimilarity: Number.isFinite(minSimilarity)\n ? minSimilarity\n : undefined,\n includeSuperseded,\n latestOnly,\n tenantId: options.tenantId ?? null,\n });\n\n return results.map(serializeFact);\n } catch (error) {\n // Gracefully handle missing facts table (cross-package dependency)\n if (\n typeof error === 'object' &&\n error !== null &&\n (error as { code?: unknown }).code === 'DB_SCHEMA_MISSING'\n ) {\n return [];\n }\n throw error;\n }\n }\n\n public async getBySlug(\n options: {\n slug?: string;\n context?: string;\n status?: string;\n tenantId?: string | null;\n } = {},\n ) {\n if (!options.slug) {\n throw new Error('slug is required');\n }\n\n const where: {\n slug: string;\n context: string;\n tenantId?: string | null;\n } = {\n slug: options.slug,\n context: options.context || '',\n };\n if (options.tenantId !== undefined) {\n where.tenantId = options.tenantId;\n }\n\n const content = await this.get(where);\n\n if (!content) {\n return null;\n }\n\n if (options.status && content.status !== options.status) {\n return null;\n }\n\n return serializeContent(content);\n }\n\n public async getGovernanceDefinitionsAction(\n options: { tenantId?: string | null } = {},\n ) {\n const [effective, persisted] = await Promise.all([\n getEffectiveContentGovernanceConfig({\n db: this.db,\n tenantId: options.tenantId,\n }),\n loadPersistedContentGovernanceDefinitions({\n db: this.db,\n tenantId: options.tenantId,\n }),\n ]);\n\n // Persisted rows list the global layer first, then the tenant's; keys\n // repeat across the layers (#3305 M1), so the tenant override is the\n // row an effective entry stands for.\n const effectiveRow = <T extends { key?: string }>(\n rows: T[],\n key?: string,\n ) => [...rows].reverse().find((item) => item.key === key) || {};\n\n return {\n effective: {\n policies: effective.policies.map((policy) => ({\n ...policy,\n ...effectiveRow(persisted.policies, policy.key),\n })),\n profiles: effective.profiles.map((profile) => ({\n ...profile,\n ...effectiveRow(persisted.profiles, profile.key),\n })),\n assignments: effective.assignments.map((assignment) => ({\n ...assignment,\n ...effectiveRow(persisted.assignments, assignment.key),\n })),\n },\n persisted: {\n policies: persisted.policies,\n profiles: persisted.profiles,\n assignments: persisted.assignments,\n },\n };\n }\n\n public async resolveGovernanceAction(\n options: {\n type?: string;\n variant?: string | null;\n tenantId?: string | null;\n } = {},\n ) {\n return resolveEffectiveContentGovernance({\n contentType: options.type || null,\n contentVariant: options.variant || null,\n db: this.db,\n tenantId: options.tenantId,\n });\n }\n\n /**\n * Mirrors content from a remote URL\n *\n * Downloads and stores content from a remote URL, extracting text\n * and saving it as a Content object.\n *\n * @param options - Mirror options\n * @param options.url - URL to mirror\n * @param options.mirrorDir - Directory for caching mirrored files\n * @param options.context - Context for the mirrored content\n * @returns Promise resolving to the mirrored Content object\n * @throws Error if URL is invalid or missing\n */\n public async mirror(options: {\n url: string;\n mirrorDir?: string;\n context?: string;\n /**\n * Skip the SSRF host-blocking checks. Only set this for fully trusted,\n * operator-supplied URLs (e.g. local development), never for URLs that\n * originate from end users or external content.\n */\n allowPrivateNetworkHosts?: boolean;\n /** Injectable DNS resolver (primarily for tests). */\n resolveHostname?: ResolveHostname;\n /** Injectable fetch for redirect resolution (primarily for tests). */\n fetchImpl?: typeof fetch;\n }) {\n if (!options.url) {\n throw new Error('No URL provided');\n }\n // Validate the URL AND block private/loopback/link-local/metadata hosts\n // before fetching — `mirror()` fetches an arbitrary caller-supplied URL,\n // which is a classic SSRF sink (e.g. http://169.254.169.254/ metadata).\n // `fetchDocument` follows redirects on its own, so we resolve the redirect\n // chain ourselves first — re-validating every hop — and hand it the\n // already-validated terminal URL, closing the public-host-30x-to-internal\n // bypass (S5 #1388 / review #1562).\n let url: URL;\n try {\n url = await resolveSafeFinalUrl(options.url, {\n allowPrivateNetworkHosts: options.allowPrivateNetworkHosts,\n resolveHostname: options.resolveHostname,\n fetchImpl: options.fetchImpl,\n });\n } catch (error) {\n // Never echo the raw URL — it may carry userinfo credentials (review #1562).\n const safeUrl = redactUrlCredentials(options.url);\n logger.error('Refusing to mirror unsafe URL', { error, url: safeUrl });\n throw new Error(`Invalid URL provided: ${safeUrl}`);\n }\n const existing = await this.get({ url: options.url });\n if (existing) {\n return existing;\n }\n\n // Fetch and process the document via the already-validated terminal URL.\n const doc = await fetchDocument(url.toString(), {\n cacheDir: options?.mirrorDir,\n });\n\n const filename = url.pathname.split('/').pop();\n const nameWithoutExtension = filename?.replace(/\\.[^/.]+$/, '');\n const title = nameWithoutExtension?.replace(/[-_]/g, ' ');\n\n // Extract text from all document parts\n const body = doc.parts.map((part) => part.content).join('\\n\\n');\n if (body) {\n const content = new Content({\n url: options.url,\n type: 'mirror',\n title,\n context: options.context || '',\n body,\n });\n await content.initialize();\n // The file name is not an identity (two sites' `index.html`), so the\n // slug is derived, not set: a taken one moves to `-2` on save instead\n // of overwriting the other mirror (the URL lookup above is the dedup).\n await content.getSlug();\n await content.save();\n return content;\n }\n }\n\n /**\n * Writes a Content object to the filesystem as a markdown file\n *\n * @param options - Options for writing the content file\n * @param options.content - Content object to write\n * @param options.contentDir - Directory to write the file to\n * @returns Promise that resolves when the file is written\n * @throws Error if contentDir is not provided\n */\n public async writeContentFile(options: {\n content: Content;\n contentDir: string;\n }) {\n const { content, contentDir } = options;\n if (!contentDir) {\n throw new Error('No content dir provided');\n }\n\n const { body } = content;\n const frontMatter = {\n title: content.title,\n slug: content.slug,\n context: content.context,\n author: content.author,\n publish_date: content.publish_date,\n };\n\n let output = '';\n if (frontMatter && Object.keys(frontMatter).length > 0) {\n output += '---\\n';\n output += YAML.stringify(frontMatter);\n output += '---\\n';\n }\n\n // Filesystem exports are markdown regardless of the editor save format.\n let formattedBody = body || '';\n const bodyFormat = resolveBodyFormat(content.bodyFormat, body);\n if (bodyFormat === 'html') {\n formattedBody = htmlToMarkdown(body || '');\n } else if (body && !this.isMarkdown(body)) {\n formattedBody = this.formatAsMarkdown(body);\n }\n output += formattedBody;\n\n const pathParts = [\n contentDir,\n content.context || '', // if empty, use empty string\n content.slug,\n 'index.md',\n ].filter(Boolean); // remove empty strings\n\n const outputFile = path.join(...(pathParts as string[]));\n\n // `context` and `slug` are persisted, caller-influenced fields. Without a\n // guard, a value like `../../etc/cron.d/x` escapes `contentDir` and lets an\n // export overwrite arbitrary files (path traversal). Confirm the joined\n // path still lives under the resolved content directory (S5 #1388).\n const resolvedDir = path.resolve(contentDir);\n const resolvedFile = path.resolve(outputFile);\n if (\n resolvedFile !== resolvedDir &&\n !resolvedFile.startsWith(resolvedDir + path.sep)\n ) {\n throw new Error(\n 'Refusing to write content file outside of the content directory',\n );\n }\n\n await ensureDirectoryExists(path.dirname(outputFile));\n await writeFile(outputFile, output);\n }\n\n /**\n * Checks if text appears to be in markdown format\n *\n * @param text - Text to check\n * @returns Boolean indicating if the text contains markdown syntax\n */\n private isMarkdown(text: string): boolean {\n // Basic check for common markdown indicators\n const markdownIndicators = [\n /^#\\s/m, // Headers\n /\\*\\*.+\\*\\*/, // Bold\n /\\*.+\\*/, // Italic\n /\\[.+\\]\\(.+\\)/, // Links\n /^\\s*[-*+]\\s/m, // Lists\n /^\\s*\\d+\\.\\s/m, // Numbered lists\n /```[\\s\\S]*```/, // Code blocks\n /^\\s*>/m, // Blockquotes\n ];\n\n return markdownIndicators.some((indicator) => indicator.test(text));\n }\n\n /**\n * Formats plain text as simple markdown\n *\n * @param text - Plain text to format\n * @returns Text formatted as basic markdown\n */\n private formatAsMarkdown(text: string): string {\n // Basic formatting of plain text to markdown\n return text\n .split(/\\n\\n+/)\n .map((paragraph) => paragraph.trim())\n .filter(Boolean)\n .join('\\n\\n');\n }\n\n /**\n * Synchronizes content to the filesystem\n *\n * Writes all article-type Content objects to the filesystem\n * as markdown files.\n *\n * @param options - Sync options\n * @param options.contentDir - Directory to write content files to\n * @returns Promise that resolves when synchronization is complete\n */\n public async syncContentDir(options: { contentDir?: string }) {\n const contentFilter = {\n type: 'article',\n };\n\n const contents = await this.list({ where: contentFilter });\n for (const content of contents) {\n await this.writeContentFile({\n content,\n contentDir: options.contentDir || this.options.contentDir || '',\n });\n }\n }\n\n /**\n * Generate thumbnails for content that doesn't have one\n *\n * @param options - Options for bulk thumbnail generation\n * @returns Promise resolving to result object with generated images and failed content IDs\n *\n * @example Generate headline cards for all published articles\n * ```typescript\n * const result = await contents.generateMissingThumbnails({\n * strategy: 'headline-card',\n * where: { type: 'article', status: 'published' },\n * brandColor: '#1a56db'\n * });\n * console.log(`Generated ${result.images.length} thumbnails`);\n * if (result.failed.length > 0) {\n * console.warn(`Failed to generate ${result.failed.length} thumbnails`);\n * }\n * ```\n */\n public async generateMissingThumbnails(\n options: GenerateMissingThumbnailsOptions,\n ): Promise<{\n images: Image[];\n failed: Array<{ contentId: string; error: string }>;\n }> {\n // Merge with thumbnail config from smrt.config.js (passed via constructor)\n // This allows CLI users to configure defaults in smrt.config.js:\n // thumbnail: { strategy: 'headline-card', brandColor: '#1976d2' }\n const configDefaults = this.options?.thumbnail || {};\n const mergedOptions = {\n ...configDefaults,\n ...options,\n };\n\n // Build query for content missing thumbnails\n const whereClause = {\n ...mergedOptions.where,\n thumbnailAssetId: null,\n };\n\n const contents = await this.list({\n where: whereClause,\n limit: mergedOptions.limit,\n });\n\n const generatedImages: Image[] = [];\n const failed: Array<{ contentId: string; error: string }> = [];\n\n for (const content of contents) {\n try {\n // Build thumbnail options based on strategy\n let thumbnailOptions: ThumbnailOptions;\n\n switch (mergedOptions.strategy) {\n case 'headline-card':\n thumbnailOptions = {\n strategy: 'headline-card',\n brandColor: mergedOptions.brandColor,\n backgroundColor: mergedOptions.backgroundColor,\n logoUrl: mergedOptions.logoUrl,\n template: mergedOptions.template,\n width: mergedOptions.width,\n height: mergedOptions.height,\n };\n break;\n\n case 'static-map':\n thumbnailOptions = {\n strategy: 'static-map',\n mapProvider: mergedOptions.mapProvider,\n zoom: mergedOptions.zoom,\n width: mergedOptions.width,\n height: mergedOptions.height,\n };\n break;\n\n case 'ai-generate':\n thumbnailOptions = {\n strategy: 'ai-generate',\n style: mergedOptions.style,\n width: mergedOptions.width,\n height: mergedOptions.height,\n ai: isAIClientOptions(this.options.ai)\n ? this.options.ai\n : undefined,\n };\n break;\n\n default:\n throw new Error(`Unknown strategy: ${mergedOptions.strategy}`);\n }\n\n const image = await content.generateThumbnail(thumbnailOptions);\n generatedImages.push(image);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n logger.error(\n `Failed to generate thumbnail for content ${content.id}: ${errorMessage}`,\n );\n failed.push({\n contentId: content.id ?? 'unknown',\n error: errorMessage,\n });\n }\n }\n\n return { images: generatedImages, failed };\n }\n\n // ============================================\n // Tenant Helper Methods\n // ============================================\n\n /**\n * Find all content belonging to a specific tenant\n *\n * @param tenantId - The tenant ID to filter by\n * @returns Promise resolving to array of Content objects for the tenant\n *\n * @example\n * ```typescript\n * const tenantContent = await contents.findByTenant('tenant-123');\n * ```\n */\n async findByTenant(tenantId: string): Promise<Content[]> {\n return this.list({ where: { tenantId } });\n }\n\n /**\n * Find all global content (not associated with any tenant).\n *\n * Routes through the shared tenant-global helper so it does not throw under\n * an active tenant context (an explicit `tenant_id IS NULL` filter would be\n * flagged as an isolation violation). (#1600)\n *\n * @returns Promise resolving to array of global Content objects\n *\n * @example\n * ```typescript\n * const globalContent = await contents.findGlobal();\n * ```\n */\n async findGlobal(): Promise<Content[]> {\n return queryGlobal<Content>(this);\n }\n\n /**\n * Find content for a tenant including global content.\n *\n * This returns both tenant-specific content and global content (tenantId is null),\n * useful for showing a tenant their content plus any shared/global resources.\n *\n * Fails closed if an active tenant context requests a different tenant's\n * rows; the admin/system path keeps the cross-tenant capability. (#1600)\n *\n * @param tenantId - The tenant ID to include\n * @returns Promise resolving to array of Content objects (tenant + global)\n *\n * @example\n * ```typescript\n * const allAccessibleContent = await contents.findWithGlobals('tenant-123');\n * ```\n */\n async findWithGlobals(tenantId: string): Promise<Content[]> {\n return queryWithGlobals<Content>(this, tenantId, 'Content.findWithGlobals');\n }\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunks/rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { A as normalizeContentTransparency, B as findTenantReferencesToGlobalKey, C as resolveContentTenantReadScope, F as smrtContentReviewPrompt, G as getContentReviewPolicy, H as getContentGovernanceConfig, I as smrtContentThumbnailAIGeneratePrompt, J as getContentReviewRequirements, K as getContentReviewProfile, L as buildContentGovernanceAssignmentKey, M as ContentReference, N as promptMessageOptions, O as getQueryRows, P as smrtContentApplyCorrectionPrompt, Q as hasStaticContentGovernanceProfile, R as buildContentReviewPrompt, S as mergeContentQueryScope, T as ThumbnailGenerator, U as getContentReviewKind, V as getAcceptedContentReviewStatuses, W as getContentReviewPolicies, X as getFallbackPolicyKind, Y as getEffectiveContentGovernanceConfig, Z as hasStaticContentGovernancePolicy, _ as assertContentQuerySchema, a as CONTENT_QUERY_IDENTITY_FIELD, at as ContentAsset, b as clearContentQuerySchemaCache, c as CONTENT_QUERY_MIN_RESULT_BYTES, d as DATA_QUERY_MAX_JSON_DEPTH, et as parseContentReviewResponse, f as DATA_QUERY_MAX_JSON_STRING_LENGTH, g as MAX_CONTENT_QUERY_OR_BRANCHES, h as DATA_QUERY_MAX_WARNING_LENGTH, i as CONTENT_QUERY_EXCLUDED_FIELD_IDS, it as ContentAssetCollection, j as ContentReferences, k as isMissingTableError, l as DATA_QUERY_FORBIDDEN_JSON_KEYS, m as DATA_QUERY_MAX_WARNINGS, n as CONTENT_QUERY_DEFAULT_PAGE_LIMIT, nt as resolveConfiguredContentGovernance, o as CONTENT_QUERY_MAX_PAGE_LIMIT, ot as isAssetAssociable, p as DATA_QUERY_MAX_STRING_LENGTH, q as getContentReviewProfileKeys, r as CONTENT_QUERY_DEFAULT_SORT, rt as resolveEffectiveContentGovernance, s as CONTENT_QUERY_MAX_RESULT_BYTES, st as isMetadataAccessor, t as CONTENT_QUERY_CLASS_NAME, tt as resetContentGovernanceConfig, u as DATA_QUERY_MAX_JSON_CONTAINER_ITEMS, v as buildContentQuerySchema, w as Content, x as executeContentQuery, y as buildDataQuerySchemaForClass, z as configureContentGovernance } from "./chunks/content-query-
|
|
2
|
+
import { A as normalizeContentTransparency, B as findTenantReferencesToGlobalKey, C as resolveContentTenantReadScope, F as smrtContentReviewPrompt, G as getContentReviewPolicy, H as getContentGovernanceConfig, I as smrtContentThumbnailAIGeneratePrompt, J as getContentReviewRequirements, K as getContentReviewProfile, L as buildContentGovernanceAssignmentKey, M as ContentReference, N as promptMessageOptions, O as getQueryRows, P as smrtContentApplyCorrectionPrompt, Q as hasStaticContentGovernanceProfile, R as buildContentReviewPrompt, S as mergeContentQueryScope, T as ThumbnailGenerator, U as getContentReviewKind, V as getAcceptedContentReviewStatuses, W as getContentReviewPolicies, X as getFallbackPolicyKind, Y as getEffectiveContentGovernanceConfig, Z as hasStaticContentGovernancePolicy, _ as assertContentQuerySchema, a as CONTENT_QUERY_IDENTITY_FIELD, at as ContentAsset, b as clearContentQuerySchemaCache, c as CONTENT_QUERY_MIN_RESULT_BYTES, d as DATA_QUERY_MAX_JSON_DEPTH, et as parseContentReviewResponse, f as DATA_QUERY_MAX_JSON_STRING_LENGTH, g as MAX_CONTENT_QUERY_OR_BRANCHES, h as DATA_QUERY_MAX_WARNING_LENGTH, i as CONTENT_QUERY_EXCLUDED_FIELD_IDS, it as ContentAssetCollection, j as ContentReferences, k as isMissingTableError, l as DATA_QUERY_FORBIDDEN_JSON_KEYS, m as DATA_QUERY_MAX_WARNINGS, n as CONTENT_QUERY_DEFAULT_PAGE_LIMIT, nt as resolveConfiguredContentGovernance, o as CONTENT_QUERY_MAX_PAGE_LIMIT, ot as isAssetAssociable, p as DATA_QUERY_MAX_STRING_LENGTH, q as getContentReviewProfileKeys, r as CONTENT_QUERY_DEFAULT_SORT, rt as resolveEffectiveContentGovernance, s as CONTENT_QUERY_MAX_RESULT_BYTES, st as isMetadataAccessor, t as CONTENT_QUERY_CLASS_NAME, tt as resetContentGovernanceConfig, u as DATA_QUERY_MAX_JSON_CONTAINER_ITEMS, v as buildContentQuerySchema, w as Content, x as executeContentQuery, y as buildDataQuerySchemaForClass, z as configureContentGovernance } from "./chunks/content-query-DcVNRGoy.js";
|
|
3
3
|
import { BODY_MAIN_IMAGE_ATTRIBUTE, BODY_THUMBNAIL_ATTRIBUTE, DEFAULT_CONTENT_BODY_FORMAT, THUMBNAIL_WIDE_ASPECT_RATIO, bodyHasImage, bodyHasThumbnail, bodyToEditorHtml, editorHtmlToBody, extractBodyImages, htmlToMarkdown, placeThumbnailInBody, removeThumbnailFromBody, renderContentBodyHtml, renderMarkdownToHtml, resolveBodyFormat, resolveBodyMainPicture, sanitizeHtml, setBodyMainImage, stripHtml, thumbnailPlacementForSize } from "./body-format.js";
|
|
4
4
|
import { n as getByTenantKey, r as resolveLookupTenantId } from "./chunks/tenant-keyed-lookup-8z_TQG3s.js";
|
|
5
5
|
import { r as ContentCorrection, t as ContentCorrectionCollection } from "./chunks/content-corrections-QyinPkfN.js";
|
|
6
6
|
import { contentEditorAssistantContextToChatProps, createContentEditorAssistantContext, sanitizeContentEditorAssistantFieldUpdates } from "./content-editor-assistant.js";
|
|
7
|
-
import { i as fetchSafeRemoteUrlInternal, r as fetchSafeRemoteUrl, t as Contents } from "./chunks/contents-
|
|
7
|
+
import { i as fetchSafeRemoteUrlInternal, r as fetchSafeRemoteUrl, t as Contents } from "./chunks/contents-DDiybAlF.js";
|
|
8
8
|
import { r as ContentReview, t as ContentReviewCollection } from "./chunks/content-reviews-H9ANWEi7.js";
|
|
9
|
-
import { r as ContentVersion, t as ContentVersionCollection } from "./chunks/content-versions-
|
|
9
|
+
import { r as ContentVersion, t as ContentVersionCollection } from "./chunks/content-versions-BnCiRXbb.js";
|
|
10
10
|
import { evaluateContentPublishReadiness } from "./publish-readiness.js";
|
|
11
11
|
import { ObjectRegistry, SmrtCollection, SmrtObject, SmrtRecipe, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
|
|
12
12
|
import { AssetCollection, AssetStatusCollection, AssetTypeCollection } from "@happyvertical/smrt-assets";
|
|
@@ -986,7 +986,7 @@ var ContentContribution = class extends SmrtObject {
|
|
|
986
986
|
return ContentContributorCollection.create(this.options);
|
|
987
987
|
}
|
|
988
988
|
async getContentsCollection() {
|
|
989
|
-
const { Contents } = await import("./chunks/contents-
|
|
989
|
+
const { Contents } = await import("./chunks/contents-DDiybAlF.js").then((n) => n.n);
|
|
990
990
|
return Contents.create({ db: this.db });
|
|
991
991
|
}
|
|
992
992
|
async getAssetCollection() {
|
package/dist/manifest.json
CHANGED
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as assertContentQuerySchema, t as CONTENT_QUERY_CLASS_NAME, v as buildContentQuerySchema, w as Content, x as executeContentQuery } from "./chunks/content-query-
|
|
1
|
+
import { _ as assertContentQuerySchema, t as CONTENT_QUERY_CLASS_NAME, v as buildContentQuerySchema, w as Content, x as executeContentQuery } from "./chunks/content-query-DcVNRGoy.js";
|
|
2
2
|
import { MAX_DATA_QUERY_PAGE_LIMIT, createDataQueryFingerprint, normalizeDataQueryRequest, startRestServer } from "@happyvertical/smrt-core";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-content",
|
|
6
|
-
"packageVersion": "0.
|
|
6
|
+
"packageVersion": "0.59.1",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
10
|
+
"manifest": "d33e74b3ab86360b82ffb1ea4e66b528ec147b35d3fa7d7c67aa2c5b5eb7a7e4",
|
|
11
|
+
"packageJson": "7f06a52c161be01ff02820458c32c68c17b9d24c1e422b596fa3ae7061e175d9",
|
|
12
12
|
"agents": "bdc3332bbd204450f2e280113336774e76f826a01e7edbd165ad1113e526b144",
|
|
13
13
|
"moduleDoc:agents/content-list.md": "76052be17883252011f074d1988df0c0353c352a18d33e2ba3c28c37e363af01"
|
|
14
14
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-content",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.1",
|
|
4
4
|
"smrtJsdoc": "strict",
|
|
5
5
|
"description": "Content processing module for SMRT framework - handles documents, web content, and media",
|
|
6
6
|
"type": "module",
|
|
@@ -56,18 +56,18 @@
|
|
|
56
56
|
"@happyvertical/logger": "^0.111.3",
|
|
57
57
|
"@happyvertical/ocr": "^0.62.1",
|
|
58
58
|
"@happyvertical/pdf": "^0.65.9",
|
|
59
|
-
"@happyvertical/smrt-assets": "0.
|
|
60
|
-
"@happyvertical/smrt-agents": "0.
|
|
61
|
-
"@happyvertical/smrt-chat": "0.
|
|
62
|
-
"@happyvertical/smrt-core": "0.
|
|
63
|
-
"@happyvertical/smrt-facts": "0.
|
|
64
|
-
"@happyvertical/smrt-images": "0.
|
|
65
|
-
"@happyvertical/smrt-messages": "0.
|
|
66
|
-
"@happyvertical/smrt-prompts": "0.
|
|
67
|
-
"@happyvertical/smrt-profiles": "0.
|
|
68
|
-
"@happyvertical/smrt-tenancy": "0.
|
|
69
|
-
"@happyvertical/smrt-types": "0.
|
|
70
|
-
"@happyvertical/smrt-ui": "0.
|
|
59
|
+
"@happyvertical/smrt-assets": "0.59.1",
|
|
60
|
+
"@happyvertical/smrt-agents": "0.59.1",
|
|
61
|
+
"@happyvertical/smrt-chat": "0.59.1",
|
|
62
|
+
"@happyvertical/smrt-core": "0.59.1",
|
|
63
|
+
"@happyvertical/smrt-facts": "0.59.1",
|
|
64
|
+
"@happyvertical/smrt-images": "0.59.1",
|
|
65
|
+
"@happyvertical/smrt-messages": "0.59.1",
|
|
66
|
+
"@happyvertical/smrt-prompts": "0.59.1",
|
|
67
|
+
"@happyvertical/smrt-profiles": "0.59.1",
|
|
68
|
+
"@happyvertical/smrt-tenancy": "0.59.1",
|
|
69
|
+
"@happyvertical/smrt-types": "0.59.1",
|
|
70
|
+
"@happyvertical/smrt-ui": "0.59.1",
|
|
71
71
|
"@happyvertical/spider": "^1.1.13",
|
|
72
72
|
"@happyvertical/sql": "^0.111.3",
|
|
73
73
|
"@happyvertical/utils": "^0.111.3",
|
|
@@ -86,9 +86,9 @@
|
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@faker-js/faker": "^10.6.0",
|
|
89
|
-
"@happyvertical/smrt-playground": "0.
|
|
90
|
-
"@happyvertical/smrt-users": "0.
|
|
91
|
-
"@happyvertical/smrt-vitest": "0.
|
|
89
|
+
"@happyvertical/smrt-playground": "0.59.1",
|
|
90
|
+
"@happyvertical/smrt-users": "0.59.1",
|
|
91
|
+
"@happyvertical/smrt-vitest": "0.59.1",
|
|
92
92
|
"@sveltejs/kit": "2.69.1",
|
|
93
93
|
"@sveltejs/package": "^2.5.8",
|
|
94
94
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|