/*
Thin HTTP client for Lagebild API POST /v1/ingress. Werkstatt does NOT
recreate Lagebild semantics locally — it only calls the public WebsiteIngress v1
contract. The client builds the payload from form data, sends it with a service
principal API key, and returns the acceptance result. Replay protection is
built-in via a stable idempotency_key (derived from submission_id + tenant).
- Do not implement Lagebild domain logic — only the HTTP call.
- Do not persist anything locally — the API owns canonical state.
- Do not retry on non-2xx — the caller decides retry strategy.
- RFC-1198: cookie-free referral attribution — /r/ partner landings + voluntary referrer field
Replace the RFC-1191 wg_ref cookie capture with URL+form attribution:
static /r/ and //r/ partner landings (noindex, zero JS,
consent-gated via publicNameConsentRef), ?ref prefill on the existing
referrerField rail, server-side slug resolution against the bundled
partners.yaml, referral_partner_slug/name on the Lagebild ingress contract,
legacy /?ref -> /r/ 302 in the generated worker, and full removal of
the cookie capture module (wg_ref, referral.attribution entitlement,
payload.attribution).
- RFC-1222: additive receipt-provenance fields on the referral block —
partner_mode, policy_effective_from, captured_at, disclosure_version.
- RFC-1239: additive process_hint pass-through — Verbund Seam C (V04,
H02). IngressProcessHint mirrors the vendored process-hint/v1 schema
byte-for-byte until AMD-WERKSTATT-02 ships a website-ingress schema
(PLAN-RFC-1238-01). The client serializes the input verbatim; detection and
mapping live in lagebild-destination-adapter.ts.
- PLAN-RFC-1238-01 (delivered): hand-written contract types retired —
all wire types now derive from the vendored verbund-integration-spec-v0.1.3
schemas via lagebild-contracts.types.generated.ts (AMD-WERKSTATT-02 shipped
website-ingress.schema.yaml in v0.1.2). This file re-exports them for
compat; only client-side types (IngressSubmitInput, IngressResult,
LagebildIngressConfig) remain hand-written — they are not contract surfaces.
Wire behaviour unchanged.
- RFC-1241: Verbund V05 — emit acquisition-context on website-ingress (first-touch UTM)
RFC-1241: sessionStorage-captured first-touch utm_* flows
browser -> send-message -> IntegrationEvent.payload.acquisition ->
lagebild destination adapter -> website-ingress/v1 acquisition field.
Producer validates the contract shape at every edge; malformed degrades
to absent, never a dispatch error. contract_version stays 1.
- shared: acquisition.ts (capture/read/isValid, storage-injected)
- ingress: IngressSubmitInput.acquisition -> wire field
- adapter: validated pass-through of payload.acquisition
- site: SendMessageRequestSchema.acquisition + client attach +
api re-validation + site-wide capture mount in layout-component
- codegen repin v0.1.3 -> v0.1.4 (VID-016 republication); generated
types unchanged in shape, new sourceHash
- conformance: emitted acquisition + emitted ingress surface validated
per snapshot (82 tests, 5 snapshots)
RFC-1097
*/
import type {
AcquisitionContext,
ExplicitPolicyAssertion,
IdentityClaim,
IngressInteractionKind,
IngressOrigin,
IngressProcessHint,
WebsiteIngressPayload,
} from "./lagebild-contracts.types.generated.ts";
export {
type AcquisitionContext,
type ExplicitPolicyAssertion,
type IdentityClaim,
type IdentityClaimType,
type IngressInteractionKind,
type IngressOrigin,
type IngressProcessHint,
type IngressProcessHintSystem,
type PolicyAssertionKind,
type WebsiteIngressPayload,
} from "./lagebild-contracts.types.generated.ts";
export const LAGEBILD_INGRESS_CONTRACT_VERSION = 1 as const;
export interface IngressResult {
readonly submission_id: string | null;
readonly accepted: boolean;
readonly is_replay: boolean;
readonly rejection_class?: string | null;
readonly correlation_id?: string | null;
}
export interface LagebildIngressConfig {
readonly apiUrl: string;
readonly apiKey: string;
readonly tenantId: string;
readonly sourceSystemId: string;
}
export interface IngressSubmitInput {
readonly submissionId: string;
readonly idempotencyKey: string;
readonly interactionKind: IngressInteractionKind;
readonly identityClaims: readonly IdentityClaim[];
readonly message?: string | null;
readonly origin?: IngressOrigin;
readonly explicitPolicyAssertions?: readonly ExplicitPolicyAssertion[];
readonly correlationId?: string | null;
readonly referralPartnerSlug?: string | null;
readonly referralPartnerName?: string | null;
readonly partnerMode?: string | null;
readonly policyEffectiveFrom?: string | null;
readonly capturedAt?: string | null;
readonly disclosureVersion?: string | null;
/** RFC-1239: already wire-shaped (snake_case) — serialized verbatim. */
readonly processHint?: IngressProcessHint;
/** RFC-1241: already wire-shaped (snake_case) — serialized verbatim. */
readonly acquisition?: AcquisitionContext;
}
/**
* Submit a website ingress event to the Lagebild API.
* Returns the acceptance result. Does not throw on non-2xx — returns
* a result with accepted=false so the caller can decide retry strategy.
*/
export async function submitIngress(
config: LagebildIngressConfig,
input: IngressSubmitInput,
): Promise {
const payload: WebsiteIngressPayload = {
contract_version: LAGEBILD_INGRESS_CONTRACT_VERSION,
tenant_id: config.tenantId,
source_system_id: config.sourceSystemId,
submission_id: input.submissionId,
idempotency_key: input.idempotencyKey,
occurred_at: new Date().toISOString(),
interaction_kind: input.interactionKind,
identity_claims: input.identityClaims,
...(input.message != null ? { message: input.message } : {}),
...(input.origin != null ? { origin: input.origin } : {}),
...(input.explicitPolicyAssertions != null
? { explicit_policy_assertions: input.explicitPolicyAssertions }
: {}),
...(input.correlationId != null ? { correlation_id: input.correlationId } : {}),
...(input.referralPartnerSlug != null
? { referral_partner_slug: input.referralPartnerSlug }
: {}),
...(input.referralPartnerName != null
? { referral_partner_name: input.referralPartnerName }
: {}),
...(input.partnerMode != null ? { partner_mode: input.partnerMode } : {}),
...(input.policyEffectiveFrom != null
? { policy_effective_from: input.policyEffectiveFrom }
: {}),
...(input.capturedAt != null ? { captured_at: input.capturedAt } : {}),
...(input.disclosureVersion != null ? { disclosure_version: input.disclosureVersion } : {}),
...(input.processHint != null ? { process_hint: input.processHint } : {}),
...(input.acquisition != null ? { acquisition: input.acquisition } : {}),
};
const url = config.apiUrl.replace(/\/$/, "") + "/v1/ingress";
let response: Response;
try {
response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + config.apiKey,
},
body: JSON.stringify(payload),
});
} catch {
return {
submission_id: input.submissionId,
accepted: false,
is_replay: false,
rejection_class: "network_error",
correlation_id: input.correlationId ?? null,
};
}
let body: unknown;
try {
body = await response.json();
} catch {
body = {};
}
const result = body as Partial;
return {
submission_id: result.submission_id ?? input.submissionId,
accepted: result.accepted ?? response.ok,
is_replay: result.is_replay ?? false,
rejection_class: result.rejection_class ?? null,
correlation_id: result.correlation_id ?? input.correlationId ?? null,
};
}
/**
* Build a stable idempotency key from tenant + submission ID.
* This ensures retries within the same submission are deduplicated
* by the Lagebild API.
*/
export function buildIdempotencyKey(tenantId: string, submissionId: string): string {
return tenantId + ":" + submissionId;
}