From a08f90868321344c9b65aa33cc568447658f111c Mon Sep 17 00:00:00 2001
From: Dave Liu <7david12liu@gmail.com>
Date: Tue, 21 Jul 2026 12:49:51 -0700
Subject: [PATCH] EVENTS-001B: pure approved-event-source admission contract
(source only)
---
SYSTEM_DESIGN.md | 26 ++
functions/eventSourceAdmission.js | 402 ++++++++++++++++
functions/eventSourceAdmission.test.js | 619 +++++++++++++++++++++++++
3 files changed, 1047 insertions(+)
create mode 100644 functions/eventSourceAdmission.js
create mode 100644 functions/eventSourceAdmission.test.js
diff --git a/SYSTEM_DESIGN.md b/SYSTEM_DESIGN.md
index afd2c8b..76faaf3 100644
--- a/SYSTEM_DESIGN.md
+++ b/SYSTEM_DESIGN.md
@@ -916,6 +916,32 @@ This contract invents no policy and duplicates no sibling. It is a *projection*,
The module is imported by no runtime or Functions index and requires only `node:util`. It reads no clock, randomness, environment, network, Firestore, or provider service; is imported by no endpoint; stores and logs nothing; and creates, publishes, or persists no event, draft, or public record. It defines no Firestore schema or Rules, runs no importer or publish worker, and wires into no endpoint or Google-Site sync. Source change, tests, merge, Firebase deployment, provider configuration, production data, website publication, and live behavior remain separate states; #399 changes no officer task and proves none of the external or live states.
+### 8.6a Approved event-source admission contract — SOURCE ONLY, UNUSED
+
+EVENTS-001B [#439](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/439) builds the second **conservative, safe-by-default** slice of parent EVENTS-001A [#121](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/121) as one unused pure contract: the **INGEST** half of the same public-event pipeline whose **PUBLISH** half is the §8.6 `projectPublicEvent` projection ([#399](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/399)). #121's flow is *Approved structured source → validate and minimize → private draft → officer review → audited publish → [§8.6] public event view*. §8.6 owns the last arrow (a private, reviewed, server-trusted record → a minimized public **view**) and is therefore a **throwing** projector. This owns the first arrow — whether **one raw, untrusted structured-source delivery** may be admitted as a **private draft** — and is therefore a **non-throwing frozen-verdict reducer** (the idiom of §8.20–§8.22): a delivery read back from a forgeable external source makes a malformed shape a *normal* adversarial event that must resolve to a reason code, never an exception a batch importer loop has to catch. The pure `classifyEventSourceAdmission(approvedSourceEvidence, deliveryEvidence)` reads the caller's owner-approved source descriptor (`{ eventSourceAdmissionSchemaVersion, sourceId, sourceKind, active }`, or a literal `null` for "no approved source") and one raw delivery whose key set is the closed public-candidate allowlist (`{ …schemaVersion, sourceId, sourceEventId, sourceRevision, eventType, title, summary, startsAt, endsAt, timezone, locationText, publicUrl, accessibilityText, protectedOfferRef }`), and returns one frozen verdict: `admit` (the delivery may become a private draft), `refused` (a policy/authorization/boundary refusal), or `denied` (malformed shape). It imports only `node:util`.
+
+```mermaid
+flowchart TD
+ S{"approved source descriptor"} -- "null (unapproved)" --> Rna["refused — source_not_approved
(payload never parsed)"]
+ S -- "malformed" --> Dsrc["denied — malformed_approved_source"]
+ S -- "inactive" --> Ria["refused — source_inactive"]
+ S -- "kind not public-eligible" --> Rknp["refused — source_kind_not_public"]
+ S -- "approved, active, public-eligible" --> D{"read untrusted delivery"}
+ D -- "extra key outside allowlist" --> Ruf["refused — unexpected_field"]
+ D -- "malformed shape / field" --> Ddel["denied — malformed_delivery"]
+ D -- "exact, well-typed" --> M{"sourceId matches descriptor?"}
+ M -- "No" --> Rmm["refused — source_mismatch"]
+ M -- "Yes" --> A["admit — private draft
lifecycle draft, published false
injective draftId"]
+```
+
+Text alternative: a null descriptor (no approved source) is refused `source_not_approved` **without the untrusted delivery ever being parsed**; a malformed descriptor is `malformed_approved_source`; an inactive source is `source_inactive`; a well-formed but non-public source kind (member-only discount, registration/form response, historical private item) is `source_kind_not_public`. Only for an approved, active, public-eligible source is the delivery read: any own key outside the closed allowlist is refused `unexpected_field`; any malformed container or ill-typed/out-of-grammar/missing field is `denied malformed_delivery`; an exact, well-typed delivery whose `sourceId` differs from the descriptor's is refused `source_mismatch`; only a matching delivery is admitted as a private draft.
+
+Four marquee safety properties, each encoded structurally rather than merely guarded. **Source-authorized:** admission requires an approved, active, public-eligible descriptor whose `sourceId` matches the delivery's, and the unapproved case refuses before the forgeable payload is examined — minimal trust, fail closed. **Public/protected boundary — allowlist, never denylist:** the delivery key set must be *exactly* the closed public-candidate allowlist, so a key outside it — which is exactly how an inlined discount or promotion code, a registration or guest list, a form response, a payment state, a Stripe id, waiver evidence, an emergency contact, a private location, a door/access instruction, a member contact, an internal note, an audit record, a provider token, or a source credential would arrive — makes the key set wrong and is refused `unexpected_field`, never admitted; because the guard is a closed allowlist, no protected-field *name* appears anywhere in the source, and an unforeseen protected field is refused the same. The only permitted protected linkage is the single opaque `protectedOfferRef` id (or explicit null), never a code or terms. **Never auto-publishes:** the sole success verdict stamps `lifecycle: 'draft'`, `published: false`, and the decision vocabulary carries no publish/approve/public verb, so no path — correct or buggy — can admit a delivery straight to public visibility; an admitted draft must still pass officer review and the §8.6 projection. **Injective idempotency key:** the emitted `draftId` is a collision-free join of (sourceId, sourceEventId, sourceRevision) over a reserved `|` delimiter that the opaque component grammar excludes, so a re-delivery of the same source revision yields the SAME id (the caller dedupes — one draft per source event revision) and a new revision a provably DISTINCT one.
+
+Content-light and defense-in-depth by construction, this contract carries only opaque handles, a closed source-kind enum, and the draft disposition — never a title, summary, location, URL, timestamp, or any event content in the verdict — and a source-boundary test enforces the absence of protected/secret vocabulary from the executable source. It validates the delivery's **shape and boundary** (each public-candidate field present, string-typed, and length-bounded for ingestion sanity; each id opaque) but **deliberately defers all public-CONTENT validation** — control characters, `<`/`>` markup, the strict https URL allowlist, UTC-timestamp component ranges, IANA timezone, `endsAt >= startsAt`, the event-type vocabulary, and the tighter per-field public bounds — to the §8.6 projection every admitted draft must still clear. Re-implementing §8.6's field validators here would duplicate a sibling; a draft this admits is never public until §8.6 clears it.
+
+This contract invents no policy and duplicates no sibling. It is the source-authorization *ingest gate* (an approved source's untrusted delivery → a private draft), distinct from §8.6's public *projection* (a trusted reviewed record → a public view), from the `commerceEventInboxDisposition` webhook inbox gate (§8.5b, Stripe event dedup — a different domain and vocabulary), from the `commerceOutboxState` delivery contract (§8.5a), and from the §8.7 operator post lifecycle and §8.22 reminder-generation reducers. It fixes no draft/review/publish workflow, audit trail, persistence, importer, minimal query index, recurrence, or source-of-record split, and invents no owner classification — those remain with #121 and its gating owner decisions [#110](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/110) (field-split approval) and [#113](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/113) (source classification). The module is imported by no runtime or Functions index and requires only `node:util`. It reads no clock, randomness, environment, network, Firestore, or provider service; is imported by no endpoint; stores and logs nothing; and creates, admits, publishes, or persists no event, draft, or public record. Source change, tests, merge, Firebase deployment, provider configuration, production data, website publication, and live behavior remain separate states; #439 changes no officer task and proves none of the external or live states.
+
### 8.7 Approval-gated social post lifecycle and audit — SOURCE ONLY, UNUSED
INSTAGRAM-002A [#401](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/401) builds the first **conservative, safe-by-default** slice of parent INSTAGRAM-002 [#92](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/92): one unused pure contract for the *approval-gated lifecycle of a social post*, in the same non-throwing frozen-verdict idiom as the commerce outbox sibling (`commerceOutboxState`, §8.5a). It governs only *whether a lifecycle transition is allowed and whether a publish is authorized*, never a caption, media reference, or provider. The pure `classifySocialPostTransition(current, command)` reducer reads the durable record `{ socialPostSchemaVersion, lifecycleStatus, sourceKind, payloadHash, approvedHash, authorActor, approverActor }` and one exact revision-1 command `{ socialPostSchemaVersion, type, expectedLifecycle, payloadHash, actor, capability, selfApprovalAllowed }`, both drawn only from closed vocabularies and opaque tokens, and returns one frozen verdict — `applied`, `unchanged`, or `rejected` with a fixed reason code — never throwing and never echoing raw input. A companion `validateSocialPostRecord` validates the same durable record and returns a frozen canonical projection or a fixed rejection reason. It imports only `node:util`.
diff --git a/functions/eventSourceAdmission.js b/functions/eventSourceAdmission.js
new file mode 100644
index 0000000..b6b4abd
--- /dev/null
+++ b/functions/eventSourceAdmission.js
@@ -0,0 +1,402 @@
+'use strict';
+
+// EVENTS-001B — pure approved-event-source admission contract (pure contract).
+//
+// SOURCE ONLY, UNUSED: this module is imported by nothing (not by
+// functions/index.js, no route, no Firestore Rules, no callable, no scheduled
+// importer). It has zero runtime behavior. It is the safety-critical decision core for
+// the *approved source contract* half of parent #121 (EVENTS-001A) — "Approve one
+// canonical public event projection AND the contract that an eventual structured source
+// must satisfy" — landed and exhaustively negative-tested first so the eventual importer
+// is a mechanical hookup of already-proven invariants. See SYSTEM_DESIGN.md §8.6a.
+//
+// It is the INGEST side of the same public-event pipeline whose PUBLISH side is the
+// §8.6 `projectPublicEvent` projection (#399). #121's flowchart is
+// Approved structured source -> Validate and minimize -> PRIVATE DRAFT
+// -> Officer review -> Audited publish -> [§8.6] Public event view
+// §8.6 owns the last arrow (a private, reviewed public-candidate record -> a minimized
+// public VIEW). This owns the first arrow — whether ONE raw structured source delivery
+// may be admitted as a PRIVATE DRAFT — and nothing between: it issues no review/publish
+// transition and grants no public visibility.
+//
+// What it decides: given the owner-approved descriptor for a source (the caller's lookup
+// result, or a literal null for "no approved source") and one raw source delivery, it
+// returns exactly one frozen verdict — `admit` (the delivery may become a private draft),
+// `refused` (a policy boundary/authorization refusal — the source is not approved, not
+// active, not a public-eligible kind, names a different source, or the delivery carries a
+// key outside the public-candidate allowlist), or `denied` (malformed shape). It is a
+// non-throwing frozen-verdict reducer (the idiom of §8.20/§8.21/§8.22): the delivery is
+// read back from an external, forgeable source, so a malformed shape is a NORMAL
+// adversarial event that must resolve to a reason-coded verdict, never an exception a
+// batch importer loop has to catch. §8.6, by contrast, is a THROWING projector because it
+// reads an already-reviewed, server-trusted record.
+//
+// Marquee safety properties, all encoded structurally rather than merely guarded:
+//
+// * SOURCE-AUTHORIZED — a delivery is admitted only when the caller supplied an
+// APPROVED, ACTIVE, PUBLIC-ELIGIBLE source descriptor whose `sourceId` MATCHES the
+// delivery's. A null descriptor (no approved source) is refused `source_not_approved`
+// WITHOUT the untrusted payload ever being parsed; an inactive source is
+// `source_inactive`; a member-only-discount / registration-response / historical
+// source kind is `source_kind_not_public`; a delivery claiming a different `sourceId`
+// than the approved descriptor authorizes is `source_mismatch`. §8.6 has no concept
+// of a source; this is the authorization gate in front of the pipeline.
+//
+// * PUBLIC/PROTECTED BOUNDARY (ALLOWLIST, NEVER DENYLIST) — the delivery's key set must
+// be EXACTLY the closed public-candidate allowlist (which includes the single opaque
+// `protectedOfferRef` linkage). A key outside that set — which is exactly how an
+// inlined discount or promotion code, a registration or guest list, a form response,
+// a payment state, a Stripe id, waiver evidence, an emergency contact, a private
+// location, a door/access instruction, a member contact, an internal note, an audit
+// record, a provider token, or a source credential would arrive — makes the key set
+// wrong and is refused `unexpected_field`, never admitted. Because the guard is a
+// closed allowlist and NOT a denylist, no protected-field NAME appears anywhere in
+// this source, and an unforeseen protected field is refused just the same. The only
+// channel by which a protected offer may be referenced is the opaque `protectedOfferRef`
+// id — never the code or terms themselves — implementing #121's "reference a separate
+// protected discount offer ONLY through a stable opaque ID."
+//
+// * NEVER AUTO-PUBLISHES — the sole success verdict stamps `lifecycle: 'draft'` and
+// `published: false`, and the decision/reason vocabulary contains NO publish, approve,
+// or public verb at all, so no code path — correct or buggy — can admit a delivery
+// straight to public visibility. An admitted delivery is a PRIVATE draft that must
+// still pass officer review and the §8.6 projection before any public exposure. This
+// is #121's "becomes a private draft only after validation ... an authorized officer
+// reviews it before an audited publish," asserted on the enums themselves.
+//
+// * INJECTIVE IDEMPOTENCY KEY — the emitted `draftId` is a deterministic, collision-free
+// join of (sourceId, sourceEventId, sourceRevision) over a reserved '|' delimiter that
+// none of the three opaque components can contain, so it is INJECTIVE: a re-delivery of
+// the same source revision yields the SAME draftId (the caller dedupes on it — one
+// draft per source event revision) and a new revision a provably DISTINCT one. This is
+// #121's "Stable source ID and revision/idempotency key."
+//
+// Content-light and defense-in-depth by construction: this contract carries only opaque
+// handles (source/event/revision refs, the opaque offer ref), a closed source-kind enum,
+// and the draft disposition — never a title, summary, location, URL, timestamp, or any
+// event content, and a source-boundary test enforces the absence of protected/secret
+// vocabulary. It validates the delivery's SHAPE and BOUNDARY (each public-candidate field
+// present, string-typed, and length-bounded for ingestion sanity; each id opaque), but
+// DELIBERATELY defers all public-CONTENT validation — control characters, `<`/`>` markup,
+// the strict https URL allowlist, UTC-timestamp component ranges, IANA timezone,
+// endsAt>=startsAt, the event-type vocabulary, and the tighter per-field public bounds —
+// to the §8.6 `projectPublicEvent` projection, which is the public gate every admitted
+// draft must still pass. Re-implementing §8.6's field validators here would duplicate a
+// sibling; a draft this admits is never public until §8.6 clears it.
+//
+// Requires only node:util. Reads no clock, env, network, Firestore, or provider.
+
+const {
+ types: { isProxy },
+} = require('node:util');
+
+const eventSourceAdmissionSchemaVersion = 1;
+
+function immutableEnum(values) {
+ return Object.freeze(Object.fromEntries(values.map((v) => [v.toUpperCase(), v])));
+}
+
+// ---- closed vocabularies -------------------------------------------------
+
+// The owner-classified kind of an approved source (#121 "Owner decisions required").
+// The first three are PUBLIC-ELIGIBLE — an event feed whose items may become a public
+// event view. The last three are approved source categories that are deliberately NOT
+// public events (a member-only discount offer, a registration/form response, or a
+// historical private item); a well-formed descriptor of such a kind is refused
+// `source_kind_not_public`, never admitted. Modelling them explicitly (rather than as
+// "unknown") lets the reducer fail closed on a real, approved-but-non-public source.
+const SOURCE_KINDS = [
+ 'mprc_hosted_event',
+ 'club_run_or_social',
+ 'third_party_race_listing',
+ 'member_only_discount_offer',
+ 'registration_or_form_response',
+ 'historical_private_item',
+];
+const PUBLIC_ELIGIBLE_KINDS = [
+ 'mprc_hosted_event',
+ 'club_run_or_social',
+ 'third_party_race_listing',
+];
+
+const ADMISSION_DECISIONS = ['admit', 'refused', 'denied'];
+// Policy/authorization/boundary refusals — the source may not (yet) produce a draft, but
+// the input is well-formed. `unexpected_field` is the public/protected boundary refusal.
+const REFUSAL_REASONS = [
+ 'source_not_approved',
+ 'source_inactive',
+ 'source_kind_not_public',
+ 'source_mismatch',
+ 'unexpected_field',
+];
+// Malformed shape — a caller/source bug or a hostile payload.
+const DENIAL_REASONS = ['malformed_approved_source', 'malformed_delivery'];
+
+const SourceKind = immutableEnum(SOURCE_KINDS);
+const AdmissionDecision = immutableEnum(ADMISSION_DECISIONS);
+const RefusalReason = immutableEnum(REFUSAL_REASONS);
+const DenialReason = immutableEnum(DENIAL_REASONS);
+
+const SOURCE_KIND_SET = new Set(SOURCE_KINDS);
+const PUBLIC_ELIGIBLE_SET = new Set(PUBLIC_ELIGIBLE_KINDS);
+
+// ---- record shapes -------------------------------------------------------
+
+// The owner-approved source descriptor: the caller's lookup result for the delivery's
+// source. TRUSTED configuration (contrast the untrusted `delivery`), kept minimal — this
+// reducer holds no registry and reads no index; the caller supplies the descriptor it
+// found (or a literal null for "no approved source"), exactly as §8.0e consumes
+// caller-supplied binding evidence rather than reading any index.
+const APPROVED_SOURCE_FIELDS = [
+ 'eventSourceAdmissionSchemaVersion',
+ 'sourceId',
+ 'sourceKind',
+ 'active',
+];
+
+// One raw structured source delivery — UNTRUSTED. Its key set is the closed
+// public-candidate allowlist: the source identity (sourceId/sourceEventId/sourceRevision),
+// the public-candidate content fields (validated for public display by §8.6, not here),
+// and the single opaque `protectedOfferRef` (an opaque id or null — the ONLY permitted
+// protected linkage). Any key outside this exact set is the public/protected boundary
+// violation `unexpected_field`.
+const DELIVERY_FIELDS = [
+ 'eventSourceAdmissionSchemaVersion',
+ 'sourceId',
+ 'sourceEventId',
+ 'sourceRevision',
+ 'eventType',
+ 'title',
+ 'summary',
+ 'startsAt',
+ 'endsAt',
+ 'timezone',
+ 'locationText',
+ 'publicUrl',
+ 'accessibilityText',
+ 'protectedOfferRef',
+];
+const DELIVERY_FIELD_SET = new Set(DELIVERY_FIELDS);
+
+// The public-candidate CONTENT fields — present, string-typed, and length-bounded here;
+// validated for public display (control chars, markup, URL scheme, timestamp ranges,
+// timezone, ordering, vocabulary) downstream by §8.6. NOT echoed in the verdict.
+const CONTENT_FIELDS = [
+ 'eventType',
+ 'title',
+ 'summary',
+ 'startsAt',
+ 'endsAt',
+ 'timezone',
+ 'locationText',
+ 'publicUrl',
+ 'accessibilityText',
+];
+
+// An opaque, store-minted, url-safe handle. The closed charset excludes the '|'
+// draft-id delimiter (so the derived id join stays injective) and every whitespace,
+// comma, quote, control, and markup character (so a handle can never smuggle a URL,
+// markup, or a delimiter into any downstream line). Bounded to reject runaway values.
+const HANDLE_PATTERN = /^[A-Za-z0-9._-]{1,256}$/;
+
+function isHandleString(value) {
+ return typeof value === 'string' && HANDLE_PATTERN.test(value);
+}
+
+// A generous ingestion-sanity ceiling for public-candidate content strings: large enough
+// for real event copy, small enough to reject a runaway or binary payload at ingest. This
+// is NOT the public-display bound — §8.6 applies the tighter per-field public limits when
+// the draft is later projected. A content field must be a non-empty string within it.
+const MAX_CONTENT_LENGTH = 8192;
+
+function isBoundedContentString(value) {
+ return typeof value === 'string' && value.length >= 1 && value.length <= MAX_CONTENT_LENGTH;
+}
+
+// Read an exact, closed record: an ordinary object whose own string-keyed properties are
+// precisely `expectedFields`, each an enumerable data property. Returns a null-prototype
+// copy read with no getter ever invoked, or null on any deviation (proxy, array, foreign
+// prototype, symbol key, wrong key count, missing, extra — enumerable OR non-enumerable —
+// inherited, accessor, or non-enumerable field).
+function readExact(value, expectedFields) {
+ if (value === null || typeof value !== 'object') return null;
+ // isProxy before Array.isArray: Array.isArray throws on a revoked proxy, while isProxy
+ // safely reports it as a proxy (which this rejects). Order matters for total,
+ // never-throwing behavior.
+ if (isProxy(value)) return null;
+ if (Array.isArray(value)) return null;
+ if (Object.getPrototypeOf(value) !== Object.prototype) return null;
+ if (Object.getOwnPropertySymbols(value).length !== 0) return null;
+ if (Object.getOwnPropertyNames(value).length !== expectedFields.length) return null;
+ const keys = Object.keys(value);
+ if (keys.length !== expectedFields.length) return null;
+ for (const key of keys) {
+ if (!expectedFields.includes(key)) return null;
+ }
+ const out = Object.create(null);
+ for (const field of expectedFields) {
+ const descriptor = Object.getOwnPropertyDescriptor(value, field);
+ if (!descriptor) return null;
+ if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) return null;
+ if (!descriptor.enumerable) return null;
+ out[field] = descriptor.value;
+ }
+ return out;
+}
+
+function readApprovedSource(value) {
+ const src = readExact(value, APPROVED_SOURCE_FIELDS);
+ if (!src) return null;
+ if (src.eventSourceAdmissionSchemaVersion !== eventSourceAdmissionSchemaVersion) return null;
+ if (!isHandleString(src.sourceId)) return null;
+ if (typeof src.sourceKind !== 'string' || !SOURCE_KIND_SET.has(src.sourceKind)) return null;
+ if (typeof src.active !== 'boolean') return null;
+ return src;
+}
+
+// Two distinct private sentinels — never exported, never returned in a verdict — so the
+// delivery read can tell "malformed shape" (a caller/source bug or hostile payload) apart
+// from "a well-formed record that violates the public/protected boundary" (an extra key)
+// by identity, and give each its own verdict.
+const DELIVERY_MALFORMED = Object.freeze(Object.create(null));
+const DELIVERY_BOUNDARY = Object.freeze(Object.create(null));
+
+// Read the untrusted delivery. Distinguishes, in this order:
+// * a hostile/ill-typed container (proxy, array, foreign prototype, symbol key) or a
+// MISSING required field or an ill-typed/out-of-grammar field -> DELIVERY_MALFORMED
+// * a well-formed container carrying an EXTRA key outside the allowlist -> DELIVERY_BOUNDARY
+// (the public/protected boundary refusal `unexpected_field`)
+// * an exact, well-typed delivery -> a null-prototype copy
+// The extra-key (boundary) check is separated from readExact so an inlined protected
+// field surfaces as a policy refusal, while a genuinely malformed shape stays a denial.
+function readDelivery(value) {
+ if (value === null || typeof value !== 'object') return DELIVERY_MALFORMED;
+ if (isProxy(value)) return DELIVERY_MALFORMED;
+ if (Array.isArray(value)) return DELIVERY_MALFORMED;
+ if (Object.getPrototypeOf(value) !== Object.prototype) return DELIVERY_MALFORMED;
+ if (Object.getOwnPropertySymbols(value).length !== 0) return DELIVERY_MALFORMED;
+
+ const names = Object.getOwnPropertyNames(value);
+ // Any own property (enumerable or not) outside the allowlist is the boundary violation
+ // — this is how an inlined protected value would arrive. Checked before missing-field
+ // and value validation so the boundary refusal is not masked by an unrelated defect.
+ for (const name of names) {
+ if (!DELIVERY_FIELD_SET.has(name)) return DELIVERY_BOUNDARY;
+ }
+ // No extra keys. Now require the exact shape: every allowlisted field present as an
+ // own enumerable data property (no getter invoked).
+ if (names.length !== DELIVERY_FIELDS.length) return DELIVERY_MALFORMED;
+ const keys = Object.keys(value);
+ if (keys.length !== DELIVERY_FIELDS.length) return DELIVERY_MALFORMED;
+
+ const out = Object.create(null);
+ for (const field of DELIVERY_FIELDS) {
+ const descriptor = Object.getOwnPropertyDescriptor(value, field);
+ if (!descriptor) return DELIVERY_MALFORMED;
+ if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) return DELIVERY_MALFORMED;
+ if (!descriptor.enumerable) return DELIVERY_MALFORMED;
+ out[field] = descriptor.value;
+ }
+
+ if (out.eventSourceAdmissionSchemaVersion !== eventSourceAdmissionSchemaVersion) {
+ return DELIVERY_MALFORMED;
+ }
+ if (!isHandleString(out.sourceId)) return DELIVERY_MALFORMED;
+ if (!isHandleString(out.sourceEventId)) return DELIVERY_MALFORMED;
+ if (!isHandleString(out.sourceRevision)) return DELIVERY_MALFORMED;
+ // protectedOfferRef is the single optional protected linkage: an opaque id or an
+ // explicit null (no offer). It is NOT permitted to be absent — the key must be present
+ // (readExact-style exact shape) — only its value may be null.
+ if (out.protectedOfferRef !== null && !isHandleString(out.protectedOfferRef)) {
+ return DELIVERY_MALFORMED;
+ }
+ // Public-candidate content fields: present, string, length-bounded (ingestion sanity).
+ // Public-display validation is §8.6's, deliberately not duplicated here.
+ for (const field of CONTENT_FIELDS) {
+ if (!isBoundedContentString(out[field])) return DELIVERY_MALFORMED;
+ }
+ return out;
+}
+
+// ---- draft identity ------------------------------------------------------
+
+const DRAFT_ID_PREFIX = 'esa1'; // event-source-admission draft identity, schema v1
+
+// A stable, deterministic, collision-free draft identity for one
+// (sourceId, sourceEventId, sourceRevision) tuple. Every component matches HANDLE_PATTERN,
+// whose charset excludes the '|' delimiter, so the join is INJECTIVE: distinct tuples
+// never collide and identical tuples always match. That injectivity is what makes "one
+// draft per source event revision" (dedupe on re-delivery) and "a new revision is a
+// genuinely new draft" provable.
+function deriveDraftId(sourceId, sourceEventId, sourceRevision) {
+ return `${DRAFT_ID_PREFIX}|${sourceId}|${sourceEventId}|${sourceRevision}`;
+}
+
+// ---- verdict constructors ------------------------------------------------
+
+// `published` is stamped `false` on the admit verdict — the machine-checkable encoding of
+// the never-auto-publish invariant. No verdict ever carries `published: true` or names an
+// approve/publish/public action; an admitted delivery is always a PRIVATE draft.
+function admit(src, del) {
+ return Object.freeze({
+ decision: 'admit',
+ reason: 'admitted',
+ draftId: deriveDraftId(del.sourceId, del.sourceEventId, del.sourceRevision),
+ sourceId: del.sourceId,
+ sourceEventId: del.sourceEventId,
+ sourceRevision: del.sourceRevision,
+ sourceKind: src.sourceKind,
+ protectedOfferRef: del.protectedOfferRef,
+ lifecycle: 'draft',
+ published: false,
+ });
+}
+
+const REFUSALS = Object.freeze(Object.fromEntries(
+ REFUSAL_REASONS.map((reason) => [reason, Object.freeze({ decision: 'refused', reason })]),
+));
+
+function refuse(reason) {
+ return REFUSALS[reason];
+}
+
+const DENIALS = Object.freeze(Object.fromEntries(
+ DENIAL_REASONS.map((reason) => [reason, Object.freeze({ decision: 'denied', reason })]),
+));
+
+function deny(reason) {
+ return DENIALS[reason];
+}
+
+// ---- the decision --------------------------------------------------------
+
+function classifyEventSourceAdmission(approvedSourceEvidence, deliveryEvidence) {
+ // A null approved-source descriptor means the caller found no approved source for this
+ // delivery. Refuse WITHOUT parsing the untrusted payload — minimal trust, fail closed:
+ // an unapproved source's delivery is never examined.
+ if (approvedSourceEvidence === null) return refuse('source_not_approved');
+
+ const src = readApprovedSource(approvedSourceEvidence);
+ if (!src) return deny('malformed_approved_source');
+ if (src.active !== true) return refuse('source_inactive');
+ if (!PUBLIC_ELIGIBLE_SET.has(src.sourceKind)) return refuse('source_kind_not_public');
+
+ const del = readDelivery(deliveryEvidence);
+ if (del === DELIVERY_MALFORMED) return deny('malformed_delivery');
+ if (del === DELIVERY_BOUNDARY) return refuse('unexpected_field');
+
+ // The approved descriptor authorizes exactly ONE sourceId; a delivery claiming any other
+ // source is refused before it can borrow this source's authorization.
+ if (del.sourceId !== src.sourceId) return refuse('source_mismatch');
+
+ return admit(src, del);
+}
+
+module.exports = Object.freeze({
+ eventSourceAdmissionSchemaVersion,
+ SourceKind,
+ AdmissionDecision,
+ RefusalReason,
+ DenialReason,
+ classifyEventSourceAdmission,
+});
diff --git a/functions/eventSourceAdmission.test.js b/functions/eventSourceAdmission.test.js
new file mode 100644
index 0000000..1e451a5
--- /dev/null
+++ b/functions/eventSourceAdmission.test.js
@@ -0,0 +1,619 @@
+'use strict';
+
+// Tests for EVENTS-001B / §8.6a — pure approved-event-source admission contract.
+//
+// Coverage: the admit happy path and identity; the source-authorization battery
+// (not-approved / inactive / non-public-kind / source-mismatch); the public/protected
+// boundary (allowlist, never denylist) crown jewel; the injective idempotency key; the
+// never-auto-publish invariant asserted structurally; malformed-approved-source and
+// malformed-delivery batteries; check-ordering (fail-closed precedence); a never-throws
+// hostile-input sweep in both argument positions; determinism/immutability/frozen
+// singletons; and comment-stripped source-boundary checks (require set = {node:util},
+// header markers, no protected/secret vocabulary in code, imported by nothing).
+
+const fs = require('fs');
+const path = require('path');
+
+const mod = require('./eventSourceAdmission');
+const {
+ eventSourceAdmissionSchemaVersion,
+ SourceKind,
+ AdmissionDecision,
+ RefusalReason,
+ DenialReason,
+ classifyEventSourceAdmission,
+} = mod;
+
+// ---- fixtures ------------------------------------------------------------
+
+function approvedSource(overrides = {}) {
+ return {
+ eventSourceAdmissionSchemaVersion: 1,
+ sourceId: 'src-mprc-cal',
+ sourceKind: 'mprc_hosted_event',
+ active: true,
+ ...overrides,
+ };
+}
+
+function delivery(overrides = {}) {
+ return {
+ eventSourceAdmissionSchemaVersion: 1,
+ sourceId: 'src-mprc-cal',
+ sourceEventId: 'evt-2026-summer-5k',
+ sourceRevision: 'rev-3',
+ eventType: 'mprc_hosted_race',
+ title: 'Summer 5K',
+ summary: 'A friendly neighborhood 5K.',
+ startsAt: '2026-08-01T15:00:00Z',
+ endsAt: '2026-08-01T17:00:00Z',
+ timezone: 'America/New_York',
+ locationText: 'City Park, North Lawn',
+ publicUrl: 'https://example.org/summer-5k',
+ accessibilityText: 'Wheelchair accessible route.',
+ protectedOfferRef: null,
+ ...overrides,
+ };
+}
+
+const ADMIT_KEYS = [
+ 'decision', 'reason', 'draftId', 'sourceId', 'sourceEventId', 'sourceRevision',
+ 'sourceKind', 'protectedOfferRef', 'lifecycle', 'published',
+];
+
+// A battery of hostile / exotic values used across never-throws and malformed suites.
+function hostileValues() {
+ const values = [
+ undefined, null, true, false, 0, 1, -1, NaN, Infinity, -Infinity, 42, 'str', '',
+ Symbol('s'), 10n, [], [1, 2, 3], {}, Object.create(null),
+ Object.create({ inherited: 1 }), new Map(), new Set(), new Date(),
+ function named() {}, () => {}, /regex/,
+ ];
+ // trap-throwing proxy
+ values.push(new Proxy({}, {
+ get() { throw new Error('get trap'); },
+ ownKeys() { throw new Error('ownKeys trap'); },
+ getOwnPropertyDescriptor() { throw new Error('descriptor trap'); },
+ getPrototypeOf() { throw new Error('proto trap'); },
+ }));
+ // revoked proxy
+ const { proxy, revoke } = Proxy.revocable({}, {});
+ revoke();
+ values.push(proxy);
+ // cyclic
+ const cyclic = {};
+ cyclic.self = cyclic;
+ values.push(cyclic);
+ // enumerable getter that throws
+ const boom = {};
+ Object.defineProperty(boom, 'title', { get() { throw new Error('boom'); }, enumerable: true, configurable: true });
+ values.push(boom);
+ return values;
+}
+
+// ---- admit happy path ----------------------------------------------------
+
+describe('admit — the happy path', () => {
+ test('an approved, active, public-eligible source with a well-formed delivery is admitted', () => {
+ const v = classifyEventSourceAdmission(approvedSource(), delivery());
+ expect(v).toEqual({
+ decision: 'admit',
+ reason: 'admitted',
+ draftId: 'esa1|src-mprc-cal|evt-2026-summer-5k|rev-3',
+ sourceId: 'src-mprc-cal',
+ sourceEventId: 'evt-2026-summer-5k',
+ sourceRevision: 'rev-3',
+ sourceKind: 'mprc_hosted_event',
+ protectedOfferRef: null,
+ lifecycle: 'draft',
+ published: false,
+ });
+ });
+
+ test('admit carries only opaque identity + disposition — no event content is echoed', () => {
+ const v = classifyEventSourceAdmission(approvedSource(), delivery());
+ expect(Object.keys(v).sort()).toEqual([...ADMIT_KEYS].sort());
+ // none of the public-candidate CONTENT fields ride into the verdict
+ for (const field of ['title', 'summary', 'startsAt', 'endsAt', 'timezone',
+ 'locationText', 'publicUrl', 'accessibilityText', 'eventType']) {
+ expect(Object.prototype.hasOwnProperty.call(v, field)).toBe(false);
+ }
+ });
+
+ test('each public-eligible source kind admits', () => {
+ for (const kind of ['mprc_hosted_event', 'club_run_or_social', 'third_party_race_listing']) {
+ const v = classifyEventSourceAdmission(approvedSource({ sourceKind: kind }), delivery());
+ expect(v.decision).toBe('admit');
+ expect(v.sourceKind).toBe(kind);
+ }
+ });
+
+ test('a protected offer may be referenced only by an opaque id, echoed verbatim', () => {
+ const v = classifyEventSourceAdmission(
+ approvedSource({ sourceKind: 'third_party_race_listing' }),
+ delivery({ protectedOfferRef: 'offer-member-15pct' }),
+ );
+ expect(v.decision).toBe('admit');
+ expect(v.protectedOfferRef).toBe('offer-member-15pct');
+ });
+
+ test('admit is always a PRIVATE draft — never auto-published', () => {
+ const v = classifyEventSourceAdmission(approvedSource(), delivery());
+ expect(v.lifecycle).toBe('draft');
+ expect(v.published).toBe(false);
+ });
+});
+
+// ---- source authorization ------------------------------------------------
+
+describe('source authorization', () => {
+ test('a null approved-source descriptor is refused source_not_approved', () => {
+ const v = classifyEventSourceAdmission(null, delivery());
+ expect(v).toEqual({ decision: 'refused', reason: 'source_not_approved' });
+ });
+
+ test('source_not_approved refuses WITHOUT parsing the untrusted delivery', () => {
+ // Garbage / hostile deliveries must not change the verdict or throw when the source
+ // is unapproved — the payload is never examined.
+ for (const bad of hostileValues()) {
+ expect(classifyEventSourceAdmission(null, bad))
+ .toEqual({ decision: 'refused', reason: 'source_not_approved' });
+ }
+ });
+
+ test('an inactive source is refused source_inactive', () => {
+ const v = classifyEventSourceAdmission(approvedSource({ active: false }), delivery());
+ expect(v).toEqual({ decision: 'refused', reason: 'source_inactive' });
+ });
+
+ test('source_inactive refuses without parsing the delivery', () => {
+ for (const bad of hostileValues()) {
+ expect(classifyEventSourceAdmission(approvedSource({ active: false }), bad))
+ .toEqual({ decision: 'refused', reason: 'source_inactive' });
+ }
+ });
+
+ test('a well-formed but non-public source kind is refused source_kind_not_public', () => {
+ for (const kind of ['member_only_discount_offer', 'registration_or_form_response', 'historical_private_item']) {
+ const v = classifyEventSourceAdmission(approvedSource({ sourceKind: kind }), delivery());
+ expect(v).toEqual({ decision: 'refused', reason: 'source_kind_not_public' });
+ }
+ });
+
+ test('source_kind_not_public refuses without parsing the delivery', () => {
+ for (const bad of hostileValues()) {
+ expect(classifyEventSourceAdmission(approvedSource({ sourceKind: 'member_only_discount_offer' }), bad))
+ .toEqual({ decision: 'refused', reason: 'source_kind_not_public' });
+ }
+ });
+
+ test('a delivery claiming a different sourceId than the descriptor is refused source_mismatch', () => {
+ const v = classifyEventSourceAdmission(
+ approvedSource({ sourceId: 'src-mprc-cal' }),
+ delivery({ sourceId: 'src-other-feed' }),
+ );
+ expect(v).toEqual({ decision: 'refused', reason: 'source_mismatch' });
+ });
+});
+
+// ---- public/protected boundary (crown jewel) -----------------------------
+
+describe('public/protected boundary — allowlist, never denylist', () => {
+ // Every one of these is an INLINED protected value that must never ride into an event
+ // record (#121 "Never public in an event record"). None is a delivery allowlist key,
+ // so each makes the key set wrong and is refused unexpected_field — regardless of its
+ // (here harmless-looking) value, and without the module ever naming the field.
+ const protectedInlines = [
+ 'discountCode', 'promotionCode', 'offerTerms', 'registrationList', 'guestList',
+ 'formResponses', 'paymentState', 'stripeId', 'waiverEvidence', 'emergencyContact',
+ 'privateLocation', 'doorInstructions', 'accessInstructions', 'memberContact',
+ 'internalNotes', 'auditRecord', 'providerToken', 'sourceCredential', 'rawHtml',
+ 'isMember', 'anythingElse',
+ ];
+
+ test.each(protectedInlines)('an inlined "%s" field is refused unexpected_field, never admitted', (field) => {
+ const v = classifyEventSourceAdmission(approvedSource(), delivery({ [field]: 'x' }));
+ expect(v).toEqual({ decision: 'refused', reason: 'unexpected_field' });
+ });
+
+ test('a NON-enumerable inlined protected field is still caught as a boundary refusal', () => {
+ const d = delivery();
+ Object.defineProperty(d, 'discountCode', { value: 'SECRET50', enumerable: false, configurable: true });
+ const v = classifyEventSourceAdmission(approvedSource(), d);
+ expect(v).toEqual({ decision: 'refused', reason: 'unexpected_field' });
+ });
+
+ test('the only permitted protected linkage is the opaque protectedOfferRef', () => {
+ // present as opaque id -> admit; explicit null -> admit; both keep the boundary intact
+ expect(classifyEventSourceAdmission(approvedSource(), delivery({ protectedOfferRef: 'offer-x' })).decision).toBe('admit');
+ expect(classifyEventSourceAdmission(approvedSource(), delivery({ protectedOfferRef: null })).decision).toBe('admit');
+ });
+
+ test('the admitted verdict can never carry a protected value — it echoes no content at all', () => {
+ // Even a delivery whose (allowlisted) content fields contain scary-looking text is
+ // admitted only as opaque identity; §8.6 is the content gate downstream.
+ const v = classifyEventSourceAdmission(approvedSource(), delivery({ summary: '' }));
+ expect(v.decision).toBe('admit');
+ expect(JSON.stringify(v)).not.toContain('