Safe vCard

Bounded RFC 6350 vCard 4.0 parser. Walks the content-line grammar (BEGIN:VCARD ... END:VCARD) into a JSON AST that the CardDAV stack stores per-tenant. Compatible with the RFC 2425 / 2426 shape that legacy CardDAV clients still emit when they negotiate VERSION:3.0; the parser admits both versions and exposes the declared VERSION field on the resulting card.

Substrate for the contacts storage protocol (b.mail.dav).

Defense posture mirrors b.safeIcal — the vCard grammar shares the line-folding + property-parameter shape with iCalendar but does not carry an RRULE-class amplifier; the equivalent amplifier here is the PHOTO / LOGO / SOUND / KEY inline-embedded-binary properties which a hostile vCard can stuff with megabytes of base64 to exhaust storage.

Caps:

- Total bytes (256 KiB strict / 1 MiB balanced / 4 MiB permissive) — refused before parsing begins. - PHOTO / LOGO / SOUND / KEY inline-embed bytes (1 MiB strict / 4 MiB balanced / 16 MiB permissive) — refused when the declared property value or data: URI body exceeds the cap. - Per-line bytes after unfolding (8 KiB strict / 32 KiB balanced / 128 KiB permissive). - Total cards in a stream (16 strict / 256 balanced / 4096 permissive). RFC 6350 §3.2 permits chained BEGIN:VCARD / END:VCARD pairs.

Header-injection / control-char defense: refuses NUL, C0 control bytes (other than TAB), and DEL (0x7F) inside property values.

Property allowlist: every property name must either appear in the RFC 6350 §6 property registry or carry the X- experimental prefix. Unknown bare names are refused.

Explicit non-goals (deferred — operator escape hatch noted):

- **vCard 4.0 to 3.0 conversion (RFC 6868)** — the parser exposes both shapes via the declared VERSION; round-tripping between them happens at the CardDAV layer when an old client requests a 4.0-only card. - **xCard XML / jCard JSON (RFC 6351 / 7095)** — the JSON AST this module emits is convertible to jCard but the framework does not currently ship the canonicalization. - **Vendor extensions** — operator extends via opts.extraProperties until the relevant slice lands.

b.safeVcard.parse(text, opts?) #

stable0.9.81
{
  profile:           "strict" | "balanced" | "permissive",         // default strict
  compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",        // -> strict
  extraProperties:   string[],   // operator-extended allowlist
}

Parse RFC 6350 vCard 4.0 text into a JSON AST. Returns { vcards: [{ version, properties: { FN: [{ params, value }], ... } }, ...] }.

Throws SafeVcardError with codes: safe-vcard/oversize-bytes / oversize-line-bytes / oversize-cards / oversize-properties-per-card / oversize-embed / missing-vcard / unterminated-vcard / unknown-property / control-char-in-value / bad-line / bad-input / bad-opt.

var ast = b.safeVcard.parse(
  "BEGIN:VCARD\r\n" +
  "VERSION:4.0\r\n" +
  "FN:Alice Example\r\n" +
  "EMAIL:alice@example.com\r\n" +
  "TEL;TYPE=cell:+1-555-0100\r\n" +
  "END:VCARD\r\n"
);
ast.vcards[0].properties.FN[0].value;     // -> "Alice Example"

b.safeVcard.compliancePosture(name) #

stable0.9.81hipaapci-dssgdprsoc2

Return the effective profile NAME for a compliance posture, or null for a name this parser does not map. Unlike the content-guard variant this returns the resolved profile string (every line-protocol parser composes gateContract.ALL_STRICT_POSTURES, so "hipaa" / "pci-dss" / "gdpr" / "soc2" all resolve to "strict") and never throws — the parser shape carries no overlay-clone, no buildProfile, and no loadRulePack. Wired by gateContract.defineParser.

b.safeVcard.compliancePosture("hipaa");                   // → "strict"
b.safeVcard.compliancePosture("not-a-regime");            // → null

Last updated 2026-08-08T16:39:49.652Z by seeder.