
Company News
Jerod Santo Joins Socket as Head of Media
Allow myself to introduce... myself.
@contentrain/types
Advanced tools
@contentrain/typesShared TypeScript types for the Contentrain ecosystem.
Start here:
This package is the common schema layer used by:
@contentrain/mcpcontentrain@contentrain/query@contentrain/rulesIt defines the stable type vocabulary for models, config, metadata, validation, scanning, context files, and provider contracts (enabling third-party RepoProvider implementations).
Use @contentrain/types when you are:
RepoProvider for a new git backendpnpm add @contentrain/types
Core unions:
FieldTypeModelKindContentStatusContentSourceWorkflowModeStackTypePlatformContextSourceCollectionRuntimeFormatLocaleStrategyCore interfaces:
FieldDefModelDefinitionModelSummaryContentrainConfigVocabularyEntryMetaAssetEntryValidationErrorValidationResultScaffoldTemplateScanCandidateDuplicateGroupGraphNodeProjectGraphScanCandidatesResultScanSummaryResultContextJsonExecution/approval unions (see Execution and approval contracts):
RiskClassApprovalGateGrantRejectionApprovalModeRunStatusScheduleKindSourceDeltaOpStorage/runtime helper types:
SingletonContentFileCollectionContentFileDictionaryContentFileCollectionEntryCollectionContentOutputDocumentEntryDocumentContentOutputSingletonMetaCollectionMetaDocumentMetaDictionaryMetaNormalize/plan types:
NormalizePlanNormalizePlanModelNormalizePlanExtractionNormalizePlanPatchProvider contracts (re-exported from provider.ts — implement these to add a new git backend):
RepoProviderRepoReaderRepoWriterProviderCapabilitiesFileChangeCommitAuthorCommitApplyPlanInputBranchFileDiffMergeResult (includes optional sync?: SyncResult for local-worktree providers)LOCAL_CAPABILITIES (const — capability set for LocalProvider)Git transaction types:
SyncResultContentrainErrorValidate functions (pure, dependency-free):
validateSlug(slug) — kebab-case slug validationvalidateEntryId(id) — entry ID format validationvalidateLocale(locale, config) — locale format + config support checkdetectSecrets(value) — detect potential secrets in field values (provider-shaped patterns, plus an api_key = … assignment whose tail passes looksLikeCredential)validateFieldValue(value, fieldDef) — full field schema validation (type, required, min/max, pattern, select)Serialize functions (pure, dependency-free):
sortKeys(obj, fieldOrder?) — recursive key sorting for canonical outputcanonicalStringify(data, fieldOrder?) — deterministic JSON serializationgenerateEntryId() — 12-char hex ID generationparseMarkdownFrontmatter(content) — parse YAML frontmatter + body from markdownserializeMarkdownFrontmatter(data, body) — serialize data + body into markdown frontmatterparseFrontmatterScalar(raw) / parseFrontmatterScalarString(raw) / splitFrontmatterList(inner) — the scalar grammar those two are built on, exported because a second reader needs it (see Frontmatter round trip)Constants:
CONTENTRAIN_DIR — default .contentrain folder nameCONTENTRAIN_BRANCH — default contentrain branch name for content trackingPATH_PATTERNS — file path conventions for models, content, meta. Content and meta paths both name the model: a document is content/{domain}/{modelId}/{slug}/{locale}.md. The patterns show the default locale_strategy: 'file'; a parity test in @contentrain/mcp holds them to what the resolvers actually produceSLUG_PATTERN — regex for valid slugsENTRY_ID_PATTERN — regex for valid entry IDsLOCALE_PATTERN — regex for valid locale codesCANONICAL_JSON — serialization rules (indent, encoding, trailing newline, key sort)SECRET_PATTERNS — provider-shaped regex patterns for secret detection (the generic api_key rule lives in detectSecrets, gated by looksLikeCredential)This package is intended to be the shared public contract across the Contentrain ecosystem.
In practice that means:
RepoProvider contract enables third-party implementations without depending on @contentrain/mcp internalsimport type {
ContentrainConfig,
FieldDef,
ModelDefinition,
ValidationResult,
} from '@contentrain/types'
const fields: Record<string, FieldDef> = {
title: { type: 'string', required: true },
slug: { type: 'slug', required: true, unique: true },
}
const model: ModelDefinition = {
id: 'blog-post',
name: 'Blog Post',
kind: 'collection',
domain: 'blog',
i18n: true,
fields,
}
const config: ContentrainConfig = {
version: 1,
stack: 'next',
workflow: 'review',
locales: { default: 'en', supported: ['en', 'tr'] },
domains: ['blog'],
}
const result: ValidationResult = {
valid: true,
errors: [],
}
Type-only usage:
import type { ModelDefinition, ContentrainConfig } from '@contentrain/types'
Mixed usage (types + runtime functions):
import type { FieldDef, ValidationError } from '@contentrain/types'
import {
validateFieldValue,
validateSlug,
detectSecrets,
canonicalStringify,
parseMarkdownFrontmatter,
} from '@contentrain/types'
Provider contract usage (for custom RepoProvider implementations):
import type { RepoProvider, ProviderCapabilities } from '@contentrain/types'
export class MyCustomProvider implements RepoProvider {
readonly capabilities: ProviderCapabilities = {
localWorktree: false,
sourceRead: true,
sourceWrite: true,
pushRemote: true,
branchProtection: true,
pullRequestFallback: true,
astScan: false,
}
// ...implement RepoProvider methods
}
Studio (Nuxt 4, web) cannot import @contentrain/mcp directly because MCP depends on Node.js-only packages (simple-git, @modelcontextprotocol/sdk). The validate and serialize functions in this package are pure, dependency-free, and browser-compatible — designed for Studio to share the same validation contract as MCP.
@contentrain/types| Function | Use case |
|---|---|
validateSlug(slug) | Form validation for document slugs |
validateEntryId(id) | Validate collection entry IDs |
validateLocale(locale, config) | Locale picker validation |
detectSecrets(value) | Content editor secret detection warnings |
validateFieldValue(value, fieldDef) | Full field-level validation in content forms |
canonicalStringify(data, fieldOrder?) | Preview canonical JSON output |
parseMarkdownFrontmatter(content) | Document editor frontmatter parsing |
serializeMarkdownFrontmatter(data, body) | Document editor serialization |
parseFrontmatterScalar(raw) | One frontmatter scalar: booleans, null, numbers, quoted strings with escapes decoded |
parseFrontmatterScalarString(raw) | The same, always as a string — a SKU of "007" must not become 7 |
splitFrontmatterList(inner) | Split an inline array on commas outside quotes |
generateEntryId() | Client-side entry ID generation |
SECRET_PATTERNS | Extend or customize secret detection |
looksLikeCredential(tail) | Decide whether a value assigned to an API-key setting is a credential or documentation |
These require file system I/O or Node.js dependencies:
checkRelation() — validates relation references against actual content files on diskvalidateProject() — full project validation with file readingwriteContent() / deleteContent() — content persistence with git worktreeresolveContentDir() / resolveJsonFilePath() — path resolution with node:pathvalidateFieldValue handles schema-level checks. Two things require external state:
These are left to Studio's server-side or API layer to implement on top of the pure validation.
@contentrain/types exists so every package in the monorepo speaks the same domain language.
Examples:
ModelDefinitionContextJsonModelDefinition and FieldDefRepoProvider to plug into MCPThis package should stay:
From the monorepo root:
pnpm --filter @contentrain/types build
pnpm --filter @contentrain/types test
pnpm --filter @contentrain/types typecheck
@contentrain/mcpcontentrain@contentrain/query@contentrain/rulesMIT
Shared shapes for the WordPress → static-site migration pipeline. They exist here — in the one MIT package every side may depend on — because the documents cross repository and license boundaries: a GPL WordPress plugin produces them, a proprietary migration service consumes them, an open emitter renders from them.
| Contract | Role |
|---|---|
RawIR | Source-faithful extraction of a WordPress site (posts, terms, menus, comments, media, redirects) with provenance: which access rung produced it (rest_public → rest_auth → wxr → bridge). Unresolved references are kept and marked, never dropped. |
CapabilityManifest | Evidence-based inventory of what the site uses (SEO, forms, comments, i18n, ACF, …) — the input for migration planning and the "what happens to X" conversation. |
ProjectIR | The reproducible model of the site: route model, layout families, component variants, query bindings, design tokens. Not "this page's HTML" — the design system that generates unseen pages correctly. |
MigrationHandoff | What the migration hands the user: repository, per-capability dispositions, and offers for runtime capabilities (with cost comparison) — offering is this document's job; fulfilling is the receiving product's. runtime (RuntimeBinding) records where the generated site's runtime components were bound once an offer was fulfilled. |
RuntimeBinding | The provider's public API origin (base_url) and project_id — all a static site needs to mount comments and forms. Never a credential: the public endpoints are unauthenticated by design. |
All are plain JSON (snake_case keys), stamped with MIGRATION_CONTRACT_VERSION.
Chrome markers the emitter honours: CHROME_BODY_SLOT (where page content goes), CHROME_REPEAT_OPEN/CHROME_IF_OPEN (per-item and conditional regions), LIST_ITEMS_SLOT (where a list section's items go) and componentSlot(id) (<!--@@component:ID@@-->, where a ComponentDef — a comments thread, a form — is mounted).
ModelDefinition can carry runtime-owned form and comments configuration.
MODEL_EXTENSION_KEYS identifies these preserved blocks; canonical model
serialization places them after fields. The content engine does not interpret
the runtime settings.
migration.ts describes a site. execution.ts describes an act upon one — an
agent run, a bulk edit, a deploy, a cutover — and who had to say yes first.
Three repositories meet here and none may define these shapes for itself. The
migration engine produces plans and receipts, Studio renders the plan card and
collects approvals, and MCP is where a plan's steps execute. If each wrote its
own RiskClass, "destructive" would mean three different things and the
approval guarding it would be theatre.
| Contract | Role |
|---|---|
RiskClass | What is at stake, as an ordered ladder: read_only → low_risk_content → bulk_content → destructive_schema → external_effect → financially_material → deployment. highestRisk() rates a plan by its worst step — a survey that ends in a deploy is a deploy. |
ApprovalGate | The three distinct decision moments: plan (before the work, on scope and cost), change (on the produced diff), release (on production effect). Approving what will be done is not approving what was produced, and neither is permission to publish it. |
ApprovalRule / ApprovalPolicyFile | .contentrain/approval-policies.json — which risk needs whose approval, in which mode (auto / single / quorum). In git, beside the content it governs, so the policy in force is the policy on the branch. Rules are additive: a policy file can only ever make a project stricter. |
ApprovalRequirement / ApprovalGrant | An outstanding demand, and a decision actually given. A grant is bound to an exact plan_hash (and commit_sha for change): change the plan and its grants stop applying. An approval of "publish these 12 posts" must not carry over to a plan that publishes 400. |
ExecutionPlan | An operation fully described before it runs: steps, union scope, risk, estimate, rollback, and the repository state it assumes. |
ExecutionReceipt | What happened: status, approvals, checkpoints, verification results, measured cost, and the scope actually touched — the same ExecutionScope shape as the plan, so prediction and outcome can be subtracted. Release approvers are the approvals entries with gate: 'release'; read them with approversFor(). |
DeploymentTarget | Where a build is published. Carries a secret_ref, never a secret — this document is written to git. |
AutomationDefinition | Reserved shape for .contentrain/automations.json. Nothing reads it yet; it exists so the first writer does not invent a fourth vocabulary for schedules. |
SourceDeltaPlan | The WordPress→repo delta — not contentrain_reconcile, which merges two git branches and knows nothing about WordPress. Carries explicit deletion tombstones, because modified_after is a filter on changed records and never reports a deletion, plus slug moves (which generate redirects) and semantic conflicts. deletions_detectable: false must not be read as "nothing was deleted". |
approval.ts is the decision procedure over these shapes: given a plan, a
policy and the decisions collected so far, may this proceed?
import { evaluateApproval, requiredApprovals } from '@contentrain/types'
// For the plan card, before anyone has decided:
requiredApprovals(plan, policy)
// → [{ gate: 'release', mode: 'quorum', min_approvals: 2, because: 'deployment' }]
// At the gate:
const decision = evaluateApproval({ plan, policy, grants, commit_sha, now })
decision.allowed // every requirement met and the plan has not expired
decision.outstanding // what is still missing, with who has signed so far
decision.rejected_grants // decisions that did not count, each with a reason
decision.reasons // one line per blocker, written for a person
It replaces a role check. shouldAutoMerge asks "is this person an owner?",
which cannot express "a bulk publish needs a second pair of eyes even from the
owner" and cannot tell a typo fix from a domain cutover. This asks about the
action: its risk, its scope, and what the project's policy says about that
combination.
Rules that matter:
effectiveRisk() takes the worst of the
plan's declared class and its steps', so a plan labelled read_only that
carries a deploy step is evaluated as a deploy.auto does not climb the ladder. Every other mode covers its class and
everything above it. If auto did too, one auto rule on a low rung would
exempt every heavier operation above it — the one mode that demands nothing
would become the only mode that can loosen a policy.allow_self_approval,
and that setting does not extend to agents.change decision is a decision about a diff. Presented with a different
branch tip than the one reviewed, it does not count.now is an input. Nothing reads the clock, so a blocked run can be
explained months later by replaying the same arguments.Every rejected decision carries a machine-readable reason
(plan_hash_mismatch, commit_mismatch, expired, agent_approver,
self_approval, role_not_permitted, duplicate_approver,
no_matching_requirement), because the useful question is never "is it
blocked" but "I approved this, why is it still blocked".
With no .contentrain/approval-policies.json, DEFAULT_APPROVAL_POLICY
applies: read-only work proceeds, everything else wants one reviewer on the
diff. Whether a project consults the evaluator at all is still governed by its
workflow setting.
plan_hashcomputePlanHash(plan) is SHA-256 over planHashPayload(plan): canonical JSON
(sorted keys, 2-space indent, trailing newline) of the plan's semantic fields.
Excluded from the payload: plan_hash itself, and id, created_at,
created_by, idempotency_key — who built a plan, when, under which run id and
with which deduplication key do not change what the plan will do, and
regenerating the same operation must produce the same hash or idempotency and
approval binding both break. Everything else is covered, so a widened scope, an
added step, a raised estimate or a withdrawn rollback each invalidate every
approval the plan had collected.
It is async because it uses Web Crypto (crypto.subtle), available in Node 18+,
Deno, Bun, workers and browsers alike — this package is consumed in all of them
and must not reach for node:crypto. A non-cryptographic hash was rejected:
approvals are pinned to this value, so a collision is an approval bypass.
The digest is reproducible from the contract alone — the test suite pins a golden hash cross-checked against an independent canonical-JSON + SHA-256 implementation in another language.
RESERVED_PATHS names four files under .contentrain/ that this repository has
claimed but does not yet write: capabilities.json, automations.json,
approval-policies.json, redirects.json. .contentrain/ is a shared
namespace — Studio, the migration engine and a customer's own tooling all write
into it — so a name claimed here cannot later be taken for something else, and
the tools that walk the directory know these four are expected rather than
stray.
Until a tool owns one, the contract is narrow and pinned by tests in
@contentrain/mcp:
contentrain_doctor and contentrain_validate ignore them completely. They
are not orphans, not broken content, and not the validator's business.contentrain_reconcile treats each as one opaque file: it takes the side that
changed it, and reports file_conflict when both sides did. It never merges
their interiors, because it does not know their interiors — a field-level
union on an approval policy would produce a policy nobody wrote.A document's fields live in YAML frontmatter, and two readers open them: the
content engine, through parseMarkdownFrontmatter (which @contentrain/mcp
re-exports), and @contentrain/query's client generator and Astro loader. The
property both depend on is that a value written and read back is the same
value — and for four shapes it did not hold.
| Value | Came back as |
|---|---|
He said "Hi" | He said \"Hi\" — the quotes were stripped without decoding the escapes |
C:\path\to | C:\\path\\to — and doubling again on every further save |
line one⏎line two | line one — the rest was written as frontmatter lines the reader then skipped |
padded | padded |
'42' (a string) | 42 (a number) |
true (a boolean) | 'true' (a string) |
All six are fixed, and the guarantee is now explicit: for every value
serializeMarkdownFrontmatter can write, parseMarkdownFrontmatter returns it
unchanged, and a second round trip produces identical bytes. The second trip
is part of the test on purpose — backslash doubling only diverges on the trip
after the one that introduced it, so a single-trip test passes on content that
corrupts a little more with every export.
What that required:
\\, \", \n, \r, \t, \uXXXX).
An unrecognised escape keeps its backslash rather than erroring — hand-written
frontmatter says "C:\Users", and losing that to strictness is a worse trade
than keeping the bytes. Text that merely starts and ends with a quote
("a" and "b") is not treated as one scalar, because slicing its ends off
would corrupt it.key: []. A bare key: is genuinely ambiguous —
empty array, empty object, or null — and the two readers were guessing it
differently.The scalar grammar is exported (parseFrontmatterScalar,
parseFrontmatterScalarString, splitFrontmatterList) and imported by the SDK
reader rather than replicated. That is the actual fix: when each side had its
own copy, correcting one of them would have turned a shared bug into a silent
disagreement between the generated client and the content engine. A parity suite
in @contentrain/query asserts both readers return the same values for the same
bytes.
Body text keeps its internal blank lines; only its leading and trailing whitespace is normalised, which is markdown behaviour rather than loss.
FAQs
Shared TypeScript types for Contentrain ecosystem
The npm package @contentrain/types receives a total of 1,206 weekly downloads. As such, @contentrain/types popularity was classified as popular.
We found that @contentrain/types demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Company News
Allow myself to introduce... myself.

Research
/Security News
A Twitch browser extension on Chrome and Firefox forwards users’ live OAuth session tokens through proxies controlled by a Russian bot service.

Security News
Anthropic found biased reasoning and recklessness drove Claude Mythos 5 to publish malware on PyPI and compromise a security vendor.