@strifeapp/strife 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content-index.d.ts +13 -0
- package/dist/content-index.js +3 -0
- package/dist/secrets.d.ts +54 -0
- package/dist/secrets.js +105 -0
- package/package.json +47 -5
- package/schema.d.ts +1 -0
- package/schema.js +5 -0
- package/types.d.ts +4 -0
- package/types.js +4 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw source of the versioned RavenDB localized content index (Jint ES5), as text.
|
|
3
|
+
*
|
|
4
|
+
* This is the versioned read-contract realization that a site deploys to its OWN
|
|
5
|
+
* database. Consumers embed this string — the Astro adapter as a RavenDB index
|
|
6
|
+
* `additionalSources` entry, the CLI `push` flow as the payload it sends to the
|
|
7
|
+
* backend conduit. It is NOT an importable/executable module: the functions inside
|
|
8
|
+
* (`map`, `load`, …) only exist in RavenDB's indexing context.
|
|
9
|
+
*
|
|
10
|
+
* The pinned `@strifeapp/strife` version is what pins this index version, so two
|
|
11
|
+
* sites on different versions deploy different index sources to different DBs.
|
|
12
|
+
*/
|
|
13
|
+
export declare const source: string;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/gen-content-index.mjs from data/ravendb/localized-content-index.js.
|
|
2
|
+
// Do not edit by hand — re-run `npm run build`.
|
|
3
|
+
export const source = "/**\n * -----------------------------------------------------------------------------\n * STRIFE CONTENT INDEX — L10N + StrifeUri + Template-Aware Relations\n * -----------------------------------------------------------------------------\n * What this does\n * Projects Strife CMS documents into a query‑friendly, localized, relation‑expanded\n * shape with one entry per locale. It keeps a deterministic output and a\n * centralized storage policy (Corax‑safe).\n * Cycle safety and performance caching.\n *\n * Canonical root fields (unchanged semantics)\n * - id, locale, displayName, url, origin, collection\n * - publishedAt, createdAt, changedAt, dependencies, labels, deleted\n *\n * Root template (whitelist + localizable)\n * - Root fields are populated from the document's template. Scalars marked\n * localizable are translated using the current entry's locale and stored.\n * - Non‑localizable scalars are index‑only at root.\n * - Arrays/objects (relations, chapters, content objects) are stored.\n * - Reserved root names can never be redefined by editors.\n *\n * Relations (template‑driven for referenced docs)\n * - Relations are detected by value shape (array of GUIDs or { id: GUID }).\n * - For each referenced GUID, the target document is loaded and projected with\n * its template (resolved by collection), using the ROOT entry's locale for\n * any localizable scalars.\n * - Order is preserved. Only GUIDs are followed. Cycles are prevented via a\n * visit cache.\n *\n * Nested content by shape (no template loads)\n * - Chapters: arrays of small objects; expanded recursively by shape.\n * - Content‑template (object): expanded recursively by shape.\n * - While expanding by shape, if a field is a relation array by shape, those\n * relations are projected via templates (same as above). Otherwise, values\n * are copied through. No per‑field localization occurs in shape expansion.\n *\n * StrifeUri\n * - Form: strife://<docId>.<collection>.<db>/<path>\n * - Resolution: loads the target doc and walks the path; the first path\n * segment is localized with the ROOT entry's locale. The resolved value can be:\n * • a scalar → copied as is\n * • an object/array → expanded by shape\n * • a relation array → projected via templates\n *\n * Storage policy (centralized at root copy step)\n * - Arrays/objects → storeAs\n * - Scalars localizable at root → storeAs (so projections return translated strings)\n * - Scalars not localizable → indexAs (leaner index)\n * - Root url is always stored\n * - Keep per‑field storage/analyzer consistent across documents (Corax‑safe)\n *\n * Caching & cycle safety\n * - docCache: documents, tplCache: templates, labelCache: labels\n * - visit cache prevents cycles and provides memoization per entry\n * - URL builder protects against origin cycles via a local visited set\n *\n * Localization model\n * - Emits one entry per configured locale.\n * - Root scalars marked localizable are translated and stored.\n * - Referenced docs projected via templates also translate localizable scalars\n * using the ROOT locale. displayName is treated as non‑localized.\n * - Shape expansion itself does not localize values, except StrifeUri's first\n * path segment at dereference time.\n *\n * Collection mapping\n * - This index must map BOTH 'Contents' AND 'Drafts' collections.\n * - Draft entries are emitted with draft: true so consumers can filter.\n * - When building URLs for draft entries, the origin chain falls back to\n * parent draft documents when the published parent lacks the locale slug.\n *\n * Determinism & engine constraints\n * - ES5‑only (no for..of, no arrow functions).\n * - Deterministic shapes under the same inputs.\n * - Relation depth is capped at MAX_RELATION_DEPTH hops; cycles are still safe.\n * -----------------------------------------------------------------------------\n */\n\n\nvar MAX_RELATION_DEPTH = 1;\n\nvar RESERVED_ROOT_FIELDS = {\n url: true,\n collection: true,\n publishedAt: true,\n createdAt: true,\n changedAt: true,\n labels: true,\n locale: true,\n origin: true,\n id: true,\n docId: true,\n displayName: true,\n dependencies: true,\n deleted: true,\n draft: true\n};\n\nfunction storeAs(name, value) {\n return { $value: value, $name: name, $options: { storage: true } };\n}\n\nfunction indexAs(name, value) {\n return { $value: value, $name: name, $options: { storage: false } };\n}\n\nfunction loadDocCached(id, docCache) {\n if (!id) return null;\n var key = id;\n if (docCache.has(key)) return docCache.get(key);\n var d = load(id, '@all_docs');\n if (d) docCache.set(key, d);\n return d;\n}\n\nfunction loadTemplateByCollectionCached(templateId, tplCache) {\n if (!templateId) return null;\n if (tplCache.has(templateId)) return tplCache.get(templateId);\n var t = load(templateId, 'templates');\n if (t) tplCache.set(templateId, t);\n return t;\n}\n\nfunction loadTemplateByIdCached(templateId, tplCache) {\n if (!templateId) return null;\n var key = templateId;\n if (tplCache.has(key)) return tplCache.get(key);\n var t = load(templateId, 'templates');\n if (t) tplCache.set(key, t);\n return t;\n}\n\nfunction loadLocalizationSettings() {\n return load('configurations/localization', 'configurations');\n}\n\nfunction loadLabelCached(id, labelCache) {\n if (!id) return null;\n if (labelCache.has(id)) return labelCache.get(id);\n var d = load(id, 'Labels');\n if (d) labelCache.set(id, d);\n return d;\n}\n\nfunction resolveSlug(slug, locale) {\n if (typeof slug === 'string') return slug; // legacy string format\n if (slug && locale && slug[locale]) return slug[locale]; // locale-keyed object\n return null;\n}\n\n// Checks if obj is a locale-keyed object where ALL keys are configured locales.\n// Uses configured locales (not BCP 47 syntax) to avoid false positives on\n// objects like { \"id\": \"abc\", \"no\": false } where keys happen to be 2-letter strings.\nfunction isLocaleObject(obj, configuredLocales) {\n if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false;\n var keys = Object.keys(obj);\n if (keys.length === 0) return false;\n for (var i = 0; i < keys.length; i++) {\n if (configuredLocales.indexOf(keys[i]) === -1) return false;\n }\n return true;\n}\n\nfunction buildUrlCached(doc, locale, docCache, isDraft) {\n var visited = {};\n var slugs = [];\n var c = doc;\n do {\n var nodeSlug = resolveSlug(c.slug, locale);\n // Draft URL building: if a published ancestor lacks the locale slug, try its draft\n if (!nodeSlug && isDraft) {\n var nodeCollection = c['@metadata'] ? c['@metadata']['@collection'] : null;\n if (nodeCollection !== 'Drafts') {\n var nodeId = c['@metadata'] ? c['@metadata']['@id'] : null;\n if (nodeId) {\n var draftNode = loadDocCached(nodeId + '/draft', docCache);\n if (draftNode) nodeSlug = resolveSlug(draftNode.slug, locale);\n }\n }\n }\n if (nodeSlug) slugs.unshift(nodeSlug);\n if (!c.origin || visited[c.origin.id]) break;\n visited[c.origin.id] = true;\n c = loadDocCached(c.origin.id, docCache);\n } while (c);\n\n // ENG-62: do NOT return slugs.shift() + slugs.join('/').\n // For an empty array that expression evaluates to the literal string \"undefined\"\n // (because [].shift() is undefined and undefined + '' coerces to \"undefined\").\n // For a two-element array it drops the separator (['a','b'] -> \"ab\" instead of \"a/b\").\n // Both behaviors are confirmed in api-test/Integration/Eng62LinkResolutionDiagnosticTests.\n //\n // We also can't just slugs.join('/'): the root home's slug is \"/\" by convention,\n // so ['/', 'page'] would produce \"//page\". Instead, normalize each slug (strip its\n // own leading/trailing slashes so \"/\" becomes \"\" and drops out) and prepend a\n // single leading \"/\" so URLs always start with \"/\".\n if (slugs.length === 0) return null;\n var parts = [];\n for (var si = 0; si < slugs.length; si++) {\n var s = slugs[si];\n if (typeof s !== 'string') continue;\n // Strip leading and trailing slashes from this segment. Reduces \"/\" to \"\"\n // (skipped) and leaves \"foo\" untouched.\n var stripped = s;\n while (stripped.length > 0 && stripped.charAt(0) === '/') stripped = stripped.substring(1);\n while (stripped.length > 0 && stripped.charAt(stripped.length - 1) === '/') stripped = stripped.substring(0, stripped.length - 1);\n if (stripped.length > 0) parts.push(stripped);\n }\n return '/' + parts.join('/');\n}\n\nfunction isLinkValue(val) {\n return val && typeof val === 'object' && !Array.isArray(val) &&\n typeof val.href === 'string' && typeof val.target === 'string' &&\n isGuid(val.id);\n}\n\nfunction resolveLinkHref(val, locale, docCache) {\n var doc = loadDocCached(val.id, docCache);\n if (!doc) return val.href;\n // ENG-62: do NOT use `'slug' in doc` here. RavenDB's Jint wraps loaded docs in a\n // BlittableObjectInstance whose [[HasProperty]] returns true for ANY property name,\n // making `'slug' in doc` always-true dead code. Use the @metadata collection check\n // (proven reliable) and a value-presence check instead.\n // See api-test/Integration/Eng62LinkResolutionDiagnosticTests for proof.\n var collection = doc['@metadata'] && doc['@metadata']['@collection'];\n if (collection === 'Files' || collection === 'Folders') return val.href;\n if (doc.slug == null) return val.href;\n var url = buildUrlCached(doc, locale, docCache);\n if (!url || url === 'undefined') return val.href;\n return url;\n}\n\nfunction resolveLink(val, locale, docCache) {\n return {\n id: val.id,\n text: val.text,\n href: resolveLinkHref(val, locale, docCache),\n target: val.target\n };\n}\n\nfunction resolveHtmlLinks(html, locale, docCache) {\n if (html.indexOf('data-id=\"') === -1) return html;\n\n var guidPat = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}';\n\n // data-id before href\n var p1 = new RegExp(\n '<a\\\\s([^>]*?)data-id=\"(' + guidPat + ')\"([^>]*?)href=\"([^\"]*)\"([^>]*?)>',\n 'g'\n );\n // ENG-62: same Jint quirk as resolveLinkHref — `'slug' in doc` is dead code on\n // a wrapped loaded doc. Use collection check + value check instead.\n var result = html.replace(p1, function(match, before, id, mid, href, after) {\n var doc = loadDocCached(id, docCache);\n if (!doc) return match;\n var coll = doc['@metadata'] && doc['@metadata']['@collection'];\n if (coll === 'Files' || coll === 'Folders') return match;\n if (doc.slug == null) return match;\n var url = buildUrlCached(doc, locale, docCache);\n if (!url || url === 'undefined') return match;\n return '<a ' + before + 'data-id=\"' + id + '\"' + mid + 'href=\"' + url + '\"' + after + '>';\n });\n\n // href before data-id\n var p2 = new RegExp(\n '<a\\\\s([^>]*?)href=\"([^\"]*)\"([^>]*?)data-id=\"(' + guidPat + ')\"([^>]*?)>',\n 'g'\n );\n result = result.replace(p2, function(match, before, href, mid, id, after) {\n var doc = loadDocCached(id, docCache);\n if (!doc) return match;\n var coll = doc['@metadata'] && doc['@metadata']['@collection'];\n if (coll === 'Files' || coll === 'Folders') return match;\n if (doc.slug == null) return match;\n var url = buildUrlCached(doc, locale, docCache);\n if (!url || url === 'undefined') return match;\n return '<a ' + before + 'href=\"' + url + '\"' + mid + 'data-id=\"' + id + '\"' + after + '>';\n });\n\n return result;\n}\n\nfunction getTranslator(locale, defaultLocale, fallbackToPrimary) {\n return function (obj) {\n if (!obj) return null;\n if (typeof obj === 'object' && obj.hasOwnProperty) {\n if (obj.hasOwnProperty(locale)) return obj[locale];\n // if (fallbackToPrimary && locale && locale.length > 2) {\n // var base = locale.slice(0, 2);\n // if (obj.hasOwnProperty(base)) return obj[base];\n // }\n //return obj[defaultLocale];\n }\n // Return non-objects as-is only for the default locale (handles false→true\n // mismatch where editor.localizable is true but stored value is still a\n // plain scalar). Non-default locales get null — the value was never translated.\n if ((typeof obj === 'string' || typeof obj === 'number') && locale === defaultLocale) return obj;\n return null;\n };\n}\n\nfunction isLocalized(obj, configuredLocales) {\n if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false;\n if (!obj.hasOwnProperty) return false;\n for (var i = 0; i < configuredLocales.length; i++) {\n if (obj.hasOwnProperty(configuredLocales[i])) return true;\n }\n return false;\n}\n\nfunction isGuid(s) {\n return typeof s === 'string' &&\n /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(s);\n}\nfunction isRefLike(x) {\n // GUID string id, or object with GUID `id`\n if (typeof x === 'string') return isGuid(x);\n if (!x || typeof x !== 'object') return false;\n return typeof x.id === 'string' && isGuid(x.id) && Object.keys(x).length <= 2;\n}\nfunction isRelationArray(v) {\n if (!v || !Array.isArray(v)) return false;\n if (v.length === 0) return false; // empty arrays are ambiguous\n for (var i = 0; i < v.length; i++) {\n if (!isRefLike(v[i])) return false;\n }\n return true;\n}\n\nvar strifeUriPattern = /^strife:\\/\\/(?<id>[-\\w_]*)\\.(?<coll>[\\w_-]*)\\.(?<db>[\\w_-]*)(?<path>[-\\w_./]*)?$/;\nfunction isStrifeUri(value) {\n var match = strifeUriPattern.exec(value || '');\n if (!match) return null;\n try {\n var path = match.groups.path || '';\n var segs = path.length > 1 ? path.slice(1).split('/') : [];\n return {\n original: value,\n db: match.groups.db,\n collection: match.groups.coll,\n docId: match.groups.id, // may be non-GUID by design for StrifeUri\n segments: segs\n };\n } catch (e) {\n return null;\n }\n}\n\nfunction tryGetReferencedValue(strifeUri, populationContext, docCache) {\n var document = loadDocCached(strifeUri.docId, docCache);\n if (!document) return null;\n\n var val = document;\n if (strifeUri.segments.length > 0) {\n // first segment may be localized; use isLocalized to distinguish locale-keyed objects\n var first = strifeUri.segments[0];\n var rawFirst = val[first];\n if (populationContext.isLocalized(rawFirst)) {\n val = populationContext.translate(rawFirst);\n } else {\n val = rawFirst;\n }\n for (var i = 1; i < strifeUri.segments.length; i++) {\n if (val == null) break;\n var segment = strifeUri.segments[i];\n val = val[segment];\n }\n }\n return val;\n}\n\nfunction resolveStrifeUriIfAny(value, populationContext, docCache) {\n var maybeUri = isStrifeUri(value);\n return maybeUri ? tryGetReferencedValue(maybeUri, populationContext, docCache) : value;\n}\n\nfunction loadRelatedLabels(labelIdsArray, labelCache) {\n if (!labelIdsArray || !labelIdsArray.length) return null;\n var out = [];\n for (var i = 0; i < labelIdsArray.length; i++) {\n var lid = labelIdsArray[i];\n var d = loadLabelCached(lid, labelCache);\n if (d) out.push(d);\n }\n return out;\n}\n\nfunction getVisitEntry(visit, id) {\n var e = visit.get(id);\n if (!e) { e = { state: 'done', node: null }; visit.set(id, e); }\n return e;\n}\n\nfunction isVisiting(visit, id) {\n var e = visit.get(id);\n return e ? e.state === 'visiting' : false;\n}\n\nfunction markVisiting(visit, id) {\n var e = getVisitEntry(visit, id);\n e.state = 'visiting';\n}\n\nfunction markDone(visit, id) {\n var e = getVisitEntry(visit, id);\n e.state = 'done';\n}\n\nfunction getCachedNode(visit, id) {\n var e = visit.get(id);\n return e && e.node ? e.node : null;\n}\n\nfunction setCachedNode(visit, id, node) {\n var e = getVisitEntry(visit, id);\n e.node = node;\n}\n\nfunction isContentTemplateNode(v) {\n return v && typeof v === 'object' && !Array.isArray(v) &&\n v['@strife'] && v['@strife'].template;\n}\n\nfunction isChaptersArray(v) {\n if (!v || !Array.isArray(v)) return false;\n if (v.length === 0) return false;\n for (var i = 0; i < v.length; i++) {\n var it = v[i];\n if (!it || !it['@strife'] || !it['@strife'].template) return false;\n }\n return true;\n}\n\nfunction populateByShape(document, out, populationContext, visit,\n docCache, tplCache, labelCache, hopsLeft) {\n if (!document || typeof document !== 'object') return;\n\n for (var prop in document) {\n if (!document || !document.hasOwnProperty || !document.hasOwnProperty(prop)) continue;\n\n // Keep metadata decorations as-is\n if (prop.charAt(0) === '@') { out[prop] = document[prop]; continue; }\n\n var val = document[prop];\n // StrifeUri resolution\n val = resolveStrifeUriIfAny(val, populationContext, docCache);\n\n // Chapters\n if (isChaptersArray(val)) {\n var arr = [];\n for (var ci = 0; ci < val.length; ci++) {\n var chapter = val[ci];\n if (chapter && chapter['@strife'] && chapter['@strife'].template) {\n var child = {};\n populateByShape(chapter, child, populationContext, visit,\n docCache, tplCache, labelCache, hopsLeft);\n arr.push(child);\n }\n }\n out[prop] = arr;\n continue;\n }\n\n // Content-template node\n if (isContentTemplateNode(val)) {\n var nested = {};\n populateByShape(val, nested, populationContext, visit,\n docCache, tplCache, labelCache, hopsLeft);\n out[prop] = nested;\n continue;\n }\n\n // Relations (GUID-strict)\n if (isRelationArray(val)) {\n var related = projectRelationsIfAny(val, populationContext, visit, docCache, tplCache, labelCache, hopsLeft);\n if (related) { out[prop] = related; continue; }\n }\n\n // Link resolution\n if (isLinkValue(val)) {\n out[prop] = resolveLink(val, populationContext.locale, docCache);\n continue;\n }\n if (Array.isArray(val) && val.length > 0 && isLinkValue(val[0])) {\n var linkArr = [];\n for (var lki = 0; lki < val.length; lki++) {\n linkArr.push(isLinkValue(val[lki]) ? resolveLink(val[lki], populationContext.locale, docCache) : val[lki]);\n }\n out[prop] = linkArr;\n continue;\n }\n\n // HTML link resolution (rich text fields)\n if (typeof val === 'string' && val.indexOf('data-id=\"') !== -1) {\n out[prop] = resolveHtmlLinks(val, populationContext.locale, docCache);\n continue;\n }\n\n // Default: copy-through (no localization inside nested)\n out[prop] = val;\n }\n\n if (document['@strife'] && !out['@strife']) out['@strife'] = document['@strife'];\n}\n\nfunction projectRelationsIfAny(value, populationContext, visit, docCache, tplCache, labelCache, hopsLeft) {\n if (!isRelationArray(value)) return null;\n if (hopsLeft <= 0) return [];\n var refs = [];\n for (var i = 0; i < value.length; i++) {\n var v = value[i];\n var idStr = (typeof v === 'string') ? v : (v && v.id);\n if (isGuid(idStr)) refs.push(idStr);\n }\n return loadRelatedDocumentsRecurse(refs, populationContext, visit, docCache, tplCache, labelCache, hopsLeft);\n}\n\nfunction populateValuesByEditor(document, template, result, populationContext, visit,\n docCache, tplCache, labelCache, isRootLevel, hopsLeft) {\n if (!template || !template.editors) return;\n var tEditors = template.editors || [];\n var i, editor;\n\n function processEditor(editor, isRoot) {\n if (!editor || !editor.editor || !editor.editor.propertyName) return;\n\n var propertyName = editor.editor.propertyName;\n\n // Never let editors redefine reserved fields (consistent everywhere)\n if (RESERVED_ROOT_FIELDS[propertyName]) return;\n\n // Read value with optional localization\n var rawVal = document[propertyName];\n\n // true→false mismatch: editor is no longer localizable but value is still a locale-object\n if (!editor.localizable && isLocaleObject(rawVal, populationContext.locales)) {\n var defVal = rawVal[populationContext.defaultLocale];\n rawVal = (defVal != null) ? defVal : rawVal[Object.keys(rawVal)[0]];\n if (rawVal == null) rawVal = null;\n }\n\n var docValue = (editor.localizable) ? populationContext.translate(rawVal) : rawVal;\n\n // StrifeUri resolution\n docValue = resolveStrifeUriIfAny(docValue, populationContext, docCache);\n\n // Relations (GUID-strict) — detect by value shape\n var relatedDocs = projectRelationsIfAny(docValue, populationContext, visit, docCache, tplCache, labelCache, hopsLeft);\n if (relatedDocs) { result[propertyName] = relatedDocs; return; }\n\n // Chapters (nested without template loads)\n if (editor.editor.type === 'chapters') {\n if (docValue == null) { // missing translation\n result[propertyName] = null; // keep null instead of dropping field\n return;\n }\n if (!Array.isArray(docValue)) return;\n var docResults = [];\n for (var ci = 0; ci < docValue.length; ci++) {\n var chapter = docValue[ci];\n if (chapter && chapter['@strife'] && chapter['@strife'].template) {\n var child = {};\n populateByShape(chapter, child, populationContext, visit,\n docCache, tplCache, labelCache, hopsLeft);\n docResults.push(child);\n }\n }\n result[propertyName] = docResults;\n return;\n }\n\n // Content-template (nested without template loads)\n if (editor.editor.type === 'content-template') {\n if (docValue == null) { // missing translation\n result[propertyName] = null;\n return;\n }\n if (docValue && typeof docValue === 'object') {\n var nested = {};\n populateByShape(docValue, nested, populationContext, visit,\n docCache, tplCache, labelCache, hopsLeft);\n result[propertyName] = nested;\n }\n return;\n }\n\n // Link resolution\n if (isLinkValue(docValue)) {\n result[propertyName] = resolveLink(docValue, populationContext.locale, docCache);\n return;\n }\n if (Array.isArray(docValue) && docValue.length > 0 && isLinkValue(docValue[0])) {\n var linkResults = [];\n for (var lki = 0; lki < docValue.length; lki++) {\n linkResults.push(isLinkValue(docValue[lki]) ? resolveLink(docValue[lki], populationContext.locale, docCache) : docValue[lki]);\n }\n result[propertyName] = linkResults;\n return;\n }\n\n // HTML link resolution (rich text fields)\n if (typeof docValue === 'string' && docValue.indexOf('data-id=\"') !== -1) {\n result[propertyName] = resolveHtmlLinks(docValue, populationContext.locale, docCache);\n return;\n }\n\n // Other fields: plain values (localized at root when editor.localizable)\n if (docValue !== undefined) {\n result[propertyName] = docValue;\n }\n }\n\n for (i = 0; i < tEditors.length; i++) {\n processEditor(tEditors[i], isRootLevel);\n }\n\n if (document['@strife']) result['@strife'] = document['@strife'];\n}\n\nfunction projectDocumentWithTemplate(doc, populationContext, visit,\n docCache, tplCache, labelCache, locale, hopsLeft) {\n var collection = doc && doc['@metadata'] ? doc['@metadata']['@collection'] : null;\n var templateId = 'templates/' + (collection === 'Drafts' ? doc['@metadata']['@base-collection'] : collection);\n var tpl = loadTemplateByCollectionCached(templateId, tplCache);\n var labelsArr = Array.isArray(doc.labels) ? doc.labels : null;\n\n var result = {\n id: doc['@metadata']['@id'],\n url: buildUrlCached(doc, locale, docCache),\n collection: collection,\n displayName: doc.displayName,\n slug: resolveSlug(doc.slug, locale),\n publishedDate: doc.publishedDate,\n labels: loadRelatedLabels(labelsArr, labelCache)\n };\n\n // Fill fields per template (use root locale via populationContext.translate)\n if (tpl && tpl.editors) {\n populateValuesByEditor(doc, tpl, result, populationContext, visit,\n docCache, tplCache, labelCache, false, hopsLeft);\n }\n\n return result;\n}\n\nfunction loadRelatedDocumentsRecurse(refGuids, populationContext, visit,\n docCache, tplCache, labelCache, hopsLeft) {\n if (!refGuids) return null;\n\n var results = [];\n for (var i = 0; i < refGuids.length; i++) {\n var id = refGuids[i];\n if (!isGuid(id)) continue;\n\n if (isVisiting(visit, id)) continue;\n\n var cached = getCachedNode(visit, id);\n if (cached) { results.push(cached); continue; }\n\n markVisiting(visit, id);\n var doc = loadDocCached(id, docCache);\n\n if (doc) {\n var projected = projectDocumentWithTemplate(doc, populationContext, visit,\n docCache, tplCache, labelCache, populationContext.locale, hopsLeft - 1);\n setCachedNode(visit, id, projected);\n results.push(projected);\n }\n\n markDone(visit, id);\n }\n\n return results;\n}\n\nfunction mapDocument(document) {\n var collection = document['@metadata']['@collection'];\n var templateId = 'templates/' + (collection === 'Drafts' ? document['@metadata']['@base-collection'] : collection);\n var template = load(templateId, 'templates');\n if (!template || !template.editors || document.deleted || document.archived) return null;\n\n var l10n = loadLocalizationSettings();\n var locales = (l10n && Array.isArray(l10n.locales)) ? l10n.locales : [l10n && l10n.defaultLocale ? l10n.defaultLocale : 'en'];\n\n var _isDraft = collection === 'Drafts';\n\n // Shared per-document caches (shared across locales)\n var _docCache = new Map();\n var _tplCache = new Map();\n var _labelCache = new Map();\n\n var entries = [];\n\n // Precompute root labels (loaded in helper)\n var rootLabelIds = Array.isArray(document.labels) ? document.labels : null;\n\n // Precompute dependencies (ES5)\n var depsArr = [];\n if (document.dependencies && Array.isArray(document.dependencies)) {\n for (var di = 0; di < document.dependencies.length; di++) {\n var dep = document.dependencies[di];\n if (dep && typeof dep.id === 'string') depsArr.push(dep.id);\n }\n }\n\n // Build a set of localizable root scalar fields from the template\n var localizableRoot = {};\n if (template && template.editors) {\n for (var li0 = 0; li0 < template.editors.length; li0++) {\n var ed0 = template.editors[li0];\n if (!ed0 || !ed0.editor || !ed0.editor.propertyName) continue;\n var t0 = ed0.editor.type;\n if (t0 !== 'related' && t0 !== 'references' && t0 !== 'content-template' && t0 !== 'chapters') {\n if (ed0.localizable === true) localizableRoot[ed0.editor.propertyName] = true;\n }\n }\n }\n\n for (var li = 0; li < locales.length; li++) {\n\n var currentLocale = locales[li];\n if (document.locales && Array.isArray(document.locales)) {\n var found = false;\n for (var k = 0; k < document.locales.length; k++) {\n if (document.locales[k] === currentLocale) { found = true; break; }\n }\n if (!found) continue;\n }\n\n var translate = getTranslator(currentLocale, l10n.defaultLocale, l10n.fallbackToPrimary);\n var checkLocalized = function (obj) { return isLocalized(obj, locales); };\n\n // Root result (stored fields)\n var result = {\n id: storeAs('id',Id(document)),\n docId: storeAs('docId', document['@metadata']['@id']),\n locale: storeAs('locale', currentLocale),\n displayName: storeAs('displayName',document.displayName),\n url: template.disableURL ? null : storeAs('url', buildUrlCached(document, currentLocale, _docCache, _isDraft)),\n origin: storeAs('origin', document.origin ? document.origin.id : null ),\n collection: storeAs('collection', collection === 'Drafts' ? document['@metadata']['@base-collection'] : collection),\n draft: storeAs('draft', (document['@metadata']['@collection'] === 'Drafts')),\n publishedAt: storeAs('publishedAt', document.publishedDate),\n createdAt: storeAs('createdAt', document.createdAt),\n changedAt: storeAs('changedAt', document.changedAt),\n dependencies: storeAs('dependencies', depsArr),\n //labels: storeAs('labels', loadRelatedLabels(rootLabelIds, _labelCache)),\n labels: storeAs('labels', loadRelatedLabels(rootLabelIds, _labelCache).map(function(label) { return label.name; })),\n deleted: indexAs('deleted', document.deleted)\n };\n\n // Visit cache per-locale (keeps shapes deterministic per entry)\n var visit = new Map();\n\n // Build dynamic fields into a temp container using ROOT TEMPLATE (respect localizable)\n var rootContainer = {};\n populateValuesByEditor(document, template, rootContainer,\n { translate: translate, isLocalized: checkLocalized, locale: currentLocale, defaultLocale: l10n.defaultLocale, locales: locales },\n visit, _docCache, _tplCache, _labelCache, true, MAX_RELATION_DEPTH);\n\n // Copy fields from temp container into result\n // - arrays/objects -> storeAs\n // - scalars -> storeAs if localizable, else indexAs\n var keys = [];\n for (var key in rootContainer) {\n if (rootContainer && rootContainer.hasOwnProperty && rootContainer.hasOwnProperty(key)) {\n keys.push(key);\n }\n }\n\n for (var ei = 0; ei < keys.length; ei++) {\n var prop = keys[ei];\n\n // never redefine canonical root fields\n if (RESERVED_ROOT_FIELDS[prop]) continue;\n\n var val = rootContainer[prop];\n var isArray = Array.isArray(val);\n var isProjectedArray = isArray && (val.length === 0 || typeof val[0] === 'object');\n var isProjectedObject = !isArray && typeof val === 'object';\n\n if (isProjectedArray || isProjectedObject) {\n result[prop] = storeAs(prop, val);\n } else {\n if (localizableRoot[prop] === true) {\n result[prop] = storeAs(prop, val); // translated scalar → store so projections return translated value\n } else {\n result[prop] = indexAs(prop, val); // non-localizable scalar → index-only\n }\n }\n }\n\n entries.push(result);\n }\n\n return entries;\n}\n";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codec for the single `STRIFE_SECRET` env var.
|
|
3
|
+
*
|
|
4
|
+
* Wire format (dot-separated, JWT-style): `v1.<meta>.<cert>`
|
|
5
|
+
* - `meta` = base64url(JSON.stringify({ urls, database, password?, type?, teamId?, previewSecret? }))
|
|
6
|
+
* - `cert` = base64url(<raw PFX bytes>) — encoded ONCE (no double-base64)
|
|
7
|
+
*
|
|
8
|
+
* This module is the single source of truth for the format. Producers (the CLI
|
|
9
|
+
* `create`/provisioning flow, generation scripts) and consumers (the generated
|
|
10
|
+
* `strife:store` module, edit-mode) both bind to it. It is intentionally
|
|
11
|
+
* dependency-free (Node `Buffer` only) so the decode logic runs on every
|
|
12
|
+
* Node-based runtime, including edge runtimes without `node:zlib`. No
|
|
13
|
+
* compression, no encryption — base64url is encoding, not secrecy.
|
|
14
|
+
*
|
|
15
|
+
* Error messages carry only STRUCTURAL diagnostics — never the blob value, the
|
|
16
|
+
* decoded meta, the certificate bytes, or the password (R7 no-leak): a decode
|
|
17
|
+
* error can surface in an SSR log or error overlay.
|
|
18
|
+
*/
|
|
19
|
+
export interface PackedSecrets {
|
|
20
|
+
urls: string[];
|
|
21
|
+
database: string;
|
|
22
|
+
/** Raw PFX bytes (NOT base64). RavenDB receives this Buffer directly. */
|
|
23
|
+
certificate: Buffer;
|
|
24
|
+
password?: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Team/workspace id. Matched against the `workspace` claim of a preview
|
|
28
|
+
* (edit-mode) token. Not secret on its own — an identifier, like an account
|
|
29
|
+
* number.
|
|
30
|
+
*/
|
|
31
|
+
teamId?: string;
|
|
32
|
+
/**
|
|
33
|
+
* HMAC secret used to verify preview (edit-mode) tokens. Distinct in purpose
|
|
34
|
+
* from the DB certificate/password, but rides in the same blob so consumers
|
|
35
|
+
* configure a single var. Consumed by edit-mode.ts.
|
|
36
|
+
*/
|
|
37
|
+
previewSecret?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Thrown on a malformed blob. Messages are structural-only (no secret content). */
|
|
40
|
+
export declare class SecretsDecodeError extends Error {
|
|
41
|
+
constructor(message: string);
|
|
42
|
+
}
|
|
43
|
+
export declare function encodeSecrets(secrets: PackedSecrets): string;
|
|
44
|
+
/**
|
|
45
|
+
* Decode a `STRIFE_SECRET` blob.
|
|
46
|
+
*
|
|
47
|
+
* @returns the packed secrets, or `null` when the blob carries a *known-shape*
|
|
48
|
+
* but unrecognised version (e.g. a future `v2`). `null` signals the caller to
|
|
49
|
+
* treat `STRIFE_SECRET` as unset and degrade to the four-var fallback chain,
|
|
50
|
+
* rather than aborting module init.
|
|
51
|
+
* @throws {SecretsDecodeError} when the blob is malformed (wrong section count,
|
|
52
|
+
* no version prefix, bad base64url, invalid/incomplete JSON).
|
|
53
|
+
*/
|
|
54
|
+
export declare function decodeSecrets(value: string): PackedSecrets | null;
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codec for the single `STRIFE_SECRET` env var.
|
|
3
|
+
*
|
|
4
|
+
* Wire format (dot-separated, JWT-style): `v1.<meta>.<cert>`
|
|
5
|
+
* - `meta` = base64url(JSON.stringify({ urls, database, password?, type?, teamId?, previewSecret? }))
|
|
6
|
+
* - `cert` = base64url(<raw PFX bytes>) — encoded ONCE (no double-base64)
|
|
7
|
+
*
|
|
8
|
+
* This module is the single source of truth for the format. Producers (the CLI
|
|
9
|
+
* `create`/provisioning flow, generation scripts) and consumers (the generated
|
|
10
|
+
* `strife:store` module, edit-mode) both bind to it. It is intentionally
|
|
11
|
+
* dependency-free (Node `Buffer` only) so the decode logic runs on every
|
|
12
|
+
* Node-based runtime, including edge runtimes without `node:zlib`. No
|
|
13
|
+
* compression, no encryption — base64url is encoding, not secrecy.
|
|
14
|
+
*
|
|
15
|
+
* Error messages carry only STRUCTURAL diagnostics — never the blob value, the
|
|
16
|
+
* decoded meta, the certificate bytes, or the password (R7 no-leak): a decode
|
|
17
|
+
* error can surface in an SSR log or error overlay.
|
|
18
|
+
*/
|
|
19
|
+
const VERSION = 'v1';
|
|
20
|
+
/** base64url alphabet, no padding — exactly what `Buffer.toString('base64url')` emits. */
|
|
21
|
+
const BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
22
|
+
/** Thrown on a malformed blob. Messages are structural-only (no secret content). */
|
|
23
|
+
export class SecretsDecodeError extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(`STRIFE_SECRET: ${message}`);
|
|
26
|
+
this.name = 'SecretsDecodeError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function encodeSecrets(secrets) {
|
|
30
|
+
const meta = {
|
|
31
|
+
urls: secrets.urls,
|
|
32
|
+
database: secrets.database,
|
|
33
|
+
};
|
|
34
|
+
// Omit empty optional fields to save bytes; decode restores absence faithfully.
|
|
35
|
+
if (secrets.password)
|
|
36
|
+
meta.password = secrets.password;
|
|
37
|
+
if (secrets.type)
|
|
38
|
+
meta.type = secrets.type;
|
|
39
|
+
if (secrets.teamId)
|
|
40
|
+
meta.teamId = secrets.teamId;
|
|
41
|
+
if (secrets.previewSecret)
|
|
42
|
+
meta.previewSecret = secrets.previewSecret;
|
|
43
|
+
const metaB64 = Buffer.from(JSON.stringify(meta), 'utf8').toString('base64url');
|
|
44
|
+
const certB64 = secrets.certificate.toString('base64url');
|
|
45
|
+
return `${VERSION}.${metaB64}.${certB64}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Decode a `STRIFE_SECRET` blob.
|
|
49
|
+
*
|
|
50
|
+
* @returns the packed secrets, or `null` when the blob carries a *known-shape*
|
|
51
|
+
* but unrecognised version (e.g. a future `v2`). `null` signals the caller to
|
|
52
|
+
* treat `STRIFE_SECRET` as unset and degrade to the four-var fallback chain,
|
|
53
|
+
* rather than aborting module init.
|
|
54
|
+
* @throws {SecretsDecodeError} when the blob is malformed (wrong section count,
|
|
55
|
+
* no version prefix, bad base64url, invalid/incomplete JSON).
|
|
56
|
+
*/
|
|
57
|
+
export function decodeSecrets(value) {
|
|
58
|
+
const sections = value.split('.');
|
|
59
|
+
if (sections.length !== 3) {
|
|
60
|
+
throw new SecretsDecodeError(`expected 3 dot-separated sections, got ${sections.length}`);
|
|
61
|
+
}
|
|
62
|
+
const [version, metaB64, certB64] = sections;
|
|
63
|
+
if (version !== VERSION) {
|
|
64
|
+
// A future versioned blob (vN, N != 1) we do not know how to read → treat as
|
|
65
|
+
// unset so resolution falls back to the individual STRIFE_* vars / options.
|
|
66
|
+
if (/^v\d+$/.test(version))
|
|
67
|
+
return null;
|
|
68
|
+
throw new SecretsDecodeError('unrecognised format (missing version prefix)');
|
|
69
|
+
}
|
|
70
|
+
if (!BASE64URL.test(metaB64)) {
|
|
71
|
+
throw new SecretsDecodeError('meta section is not valid base64url');
|
|
72
|
+
}
|
|
73
|
+
if (!BASE64URL.test(certB64)) {
|
|
74
|
+
throw new SecretsDecodeError('cert section is not valid base64url');
|
|
75
|
+
}
|
|
76
|
+
let meta;
|
|
77
|
+
try {
|
|
78
|
+
meta = JSON.parse(Buffer.from(metaB64, 'base64url').toString('utf8'));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Swallow the original error — it can quote the malformed (secret-bearing) input.
|
|
82
|
+
throw new SecretsDecodeError('meta section is not valid JSON');
|
|
83
|
+
}
|
|
84
|
+
if (typeof meta !== 'object' ||
|
|
85
|
+
meta === null ||
|
|
86
|
+
!Array.isArray(meta.urls) ||
|
|
87
|
+
typeof meta.database !== 'string') {
|
|
88
|
+
throw new SecretsDecodeError('meta section missing required fields (urls, database)');
|
|
89
|
+
}
|
|
90
|
+
const m = meta;
|
|
91
|
+
const result = {
|
|
92
|
+
urls: m.urls,
|
|
93
|
+
database: m.database,
|
|
94
|
+
certificate: Buffer.from(certB64, 'base64url'),
|
|
95
|
+
};
|
|
96
|
+
if (m.password !== undefined)
|
|
97
|
+
result.password = m.password;
|
|
98
|
+
if (m.type !== undefined)
|
|
99
|
+
result.type = m.type;
|
|
100
|
+
if (m.teamId !== undefined)
|
|
101
|
+
result.teamId = m.teamId;
|
|
102
|
+
if (m.previewSecret !== undefined)
|
|
103
|
+
result.previewSecret = m.previewSecret;
|
|
104
|
+
return result;
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,23 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strifeapp/strife",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "The Strife SDK — content-preview runtime plus schema authoring, the versioned content-index, types, and the STRIFE_SECRET codec for a Strife-backed site.",
|
|
5
8
|
"type": "module",
|
|
6
9
|
"keywords": [],
|
|
7
10
|
"files": [
|
|
8
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"schema.js",
|
|
13
|
+
"schema.d.ts",
|
|
14
|
+
"types.js",
|
|
15
|
+
"types.d.ts",
|
|
16
|
+
"content-index.d.ts"
|
|
9
17
|
],
|
|
10
18
|
"source": "index.js",
|
|
11
19
|
"module": "dist/index.js",
|
|
12
20
|
"unpkg": "dist/index.js",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./schema": {
|
|
27
|
+
"types": "./schema.d.ts",
|
|
28
|
+
"import": "./schema.js",
|
|
29
|
+
"default": "./schema.js"
|
|
30
|
+
},
|
|
31
|
+
"./types": {
|
|
32
|
+
"types": "./types.d.ts",
|
|
33
|
+
"import": "./types.js",
|
|
34
|
+
"default": "./types.js"
|
|
35
|
+
},
|
|
36
|
+
"./secrets": {
|
|
37
|
+
"types": "./dist/secrets.d.ts",
|
|
38
|
+
"import": "./dist/secrets.js",
|
|
39
|
+
"default": "./dist/secrets.js"
|
|
40
|
+
},
|
|
41
|
+
"./content-index": {
|
|
42
|
+
"types": "./content-index.d.ts",
|
|
43
|
+
"import": "./dist/content-index.js",
|
|
44
|
+
"default": "./dist/content-index.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
13
47
|
"scripts": {
|
|
14
|
-
"build": "
|
|
48
|
+
"build": "npm run clean && rollup -c -i index.js -d dist && tsc -p tsconfig.build.json && node scripts/gen-content-index.mjs",
|
|
49
|
+
"test": "vitest run",
|
|
15
50
|
"prepublishOnly": "npm run clean && npm run build",
|
|
16
51
|
"clean": "rimraf dist"
|
|
17
52
|
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@strifeapp/schema": "^0.2.0",
|
|
55
|
+
"@strifeapp/types": "^0.1.1"
|
|
56
|
+
},
|
|
18
57
|
"devDependencies": {
|
|
19
58
|
"@rollup/plugin-terser": "^1.0.0",
|
|
59
|
+
"@types/node": "^22.7.5",
|
|
20
60
|
"rimraf": "^6.1.3",
|
|
21
|
-
"rollup": "^4.0.0"
|
|
61
|
+
"rollup": "^4.0.0",
|
|
62
|
+
"typescript": "^5.7.3",
|
|
63
|
+
"vitest": "^2.1.9"
|
|
22
64
|
}
|
|
23
65
|
}
|
package/schema.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@strifeapp/schema';
|
package/schema.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Umbrella subpath: re-export the build-time schema authoring + codegen surface.
|
|
2
|
+
// One pinned @strifeapp/strife version governs the whole site, including the DSL.
|
|
3
|
+
// Node/build-time only (jiti/tinyglobby live behind @strifeapp/schema) — never
|
|
4
|
+
// imported by the browser preview entry (`.`).
|
|
5
|
+
export * from '@strifeapp/schema';
|
package/types.d.ts
ADDED
package/types.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Umbrella subpath (runtime): re-export @strifeapp/types. The source is type-only,
|
|
2
|
+
// so this is an effectively empty runtime module — it exists so the subpath resolves
|
|
3
|
+
// as a normal module for TS/bundlers (the `types` condition supplies the declarations).
|
|
4
|
+
export * from '@strifeapp/types';
|