/** * Fragments — the core client for Declarative Context rung 2 (spec §2.3/§4.3). * * The keys and fallback strings the developer writes ARE the request schema: * there is nothing to create in a dashboard before the first render. The flow * is fixed and deliberately boring: * * 1. The caller's fallbacks are the result shape, synchronously computable. * 2. ctxhash + schemahash are computed locally (declared dims ONLY — env * signals never enter the fragment identity, so cardinality stays "the * number of call sites in the org's source"). * 3. One cacheable GET. Warm hit → serve those fields. * 4. On a miss with `pending: true`, ONE POST /mint, deduplicated per * session. The server generates ON THIS REQUEST and streams the copy * back (SSE: `accepted`, then `fragments` or `done`), so the visitor * whose page asked is the visitor who gets the words — inside the * renderer's hold budget when generation is quick enough (otherwise the * fallback has landed and stays), and from the warm read on every view * after. A server that still answers 202 JSON * (the pre-request-time contract) is tolerated: the fallbacks stand. * * `fetchFragments` NEVER rejects. Any failure — network, malformed body, * hashing, a hostile response — resolves to fallback-shaped fields with * source "static". The developer's copy is the floor. */ import { type DeclaredContext } from "./context.js"; import type { FieldState } from "./state.js"; import type { ContentSource, FragmentKind, FragmentWarmth } from "./wire.js"; /** * Whether a response that arrives AFTER a field's fallback has landed may * replace it. It may not: text a visitor can see is never replaced. * * `"never"` — the behaviour of every field. A field holds (reserved, * invisible) while the response is in flight; a response inside the hold * lands in the reserved space, and once the fallback has landed it is final * for the view. * * `"before-paint"` and `"always"` — deprecated. Still accepted, so existing * call sites keep compiling, and they now behave exactly like `"never"`: no * late response replaces a landed fallback. Presentation only: the policy * never reaches the wire and never enters the schema hash. */ export type FragmentSwapPolicy = "always" | "before-paint" | "never"; /** * Bare-string shorthand: `"Sign in"` means `{ kind: "body", fallback: "Sign in" }`. * * An ARRAY fallback declares one paragraph per element: `intro` with three * elements expands to the ordinary fields `intro1`, `intro2`, `intro3` before * anything is hashed or sent — the wire never learns arrays exist. One kind * per array, applied to every element; per-paragraph budgets, so each * paragraph's envelope is its own and a whole section stays layout-stable. * The array is a DECLARATION, not data: it must be literal in your source, * because its length is part of the schema hash — a different paragraph * count is a different construction. */ export type FragmentFieldSpec = string | { kind?: FragmentKind; fallback: string | string[]; /** Late-swap policy. Every value behaves as `"never"`; the other * values are deprecated (see `FragmentSwapPolicy`). */ swap?: FragmentSwapPolicy; }; /** * The swap policy that applies to a field: `"never"`, whatever the field * declares. The deprecated `"before-paint"` and `"always"` are accepted and * behave as `"never"`. (`spec` and `kind` are accepted for compatibility.) */ export declare function fragmentSwapPolicy(_spec?: FragmentFieldSpec, _kind?: FragmentKind): FragmentSwapPolicy; /** * A signed mint token, or a function producing one. * * The token is minted by the CUSTOMER'S BACKEND (POST * https://app.sparkletree.io/api/embed/v1/fragments/mint-token with an * org-pinned `st_` key; this is the app host, not the embed `apiBase`, which * does not serve this endpoint) and is bound to one (org, ctxhash, schemahash) * for 300 seconds. The browser only echoes it. The callback form exists * because of that TTL: a page open longer than five minutes needs a fresh * token at mint time, not the one that shipped with the HTML. * * The callback receives the write it is signing for — the same declared dims * and normalized schema the mint POST carries, plus both hashes — because a * token is bound to exactly one (contextHash, schemaHash). Forward `context` * and `schema` to your backend's mint-token call as-is; a page with several * `useFragments` declarations gets a matching token for each. A callback * that ignores its argument (the pre-0.11.2 form) still compiles and runs. */ export type MintTokenSource = string | ((request: MintTokenRequest) => string | null | undefined | Promise); /** What a mint token is requested for: the exact write it will authorize. */ export interface MintTokenRequest { organizationId: string; /** Declared dims, short-keyed — the `context` field of the mint POST body. */ context: Record; /** Normalized field schema — the `schema` field of the mint POST body. */ schema: NormalizedFragmentField[]; contextHash: string; schemaHash: string; } export interface FetchFragmentsOptions { organizationId: string; apiBase: string; context?: DeclaredContext; fields: Record; /** Sent on the mint POST only — never on the GET, which is a shared CDN * object and must be byte-identical for every caller. Optional: without it * the mint goes unsigned and the server admits it or not. */ mintToken?: MintTokenSource; /** `st_pk_…` from the dashboard. Sent on the mint POST only, as * `Authorization: Bearer ` — never on the GET, for the same reason as * the token. A live key sent from an origin registered to the org is enough * for the server to admit the mint, so a site with no backend (a brand page) * gets live copy. When `mintToken` is also given, both are sent and the * server prefers the token. */ publishableKey?: string; /** Where `apiBase` came from. The SDK cannot verify a key's signature (it * holds no secret), so a key-derived apiBase is still an unproven host * client-side: it earns content fetches but NOT the mint POST (which * carries the signed mint token and the declared dims) unless the host is * first-party or loopback (S-2). Defaults to "explicit": a caller passing * apiBase directly has chosen it themselves. */ apiBaseProvenance?: "explicit" | "key"; /** Test seam; defaults to the platform fetch. */ fetchImpl?: typeof fetch; /** Called once when the server has accepted a request-time generation and * is writing the copy for this very page view. A renderer holding the * canvas may restart its budget ONCE here — the same rule as an island * stream proving it is alive (`copyStart`). */ onMintStarted?: () => void; } export interface FragmentsResult { /** One FieldState per requested key — the exact shape islands produce. */ fields: Record; /** Locally computed declared-context hash. Third leg of the pin key. */ contextHash: string; warmth: FragmentWarmth; /** decideChrome over the strongest served provenance (cached counts as * adapted). The group-level Art. 50 disclosure hangs off this. */ marked: boolean; } /** The normalized request schema: what /mint receives, what schemahash covers. */ export interface NormalizedFragmentField { key: string; kind: FragmentKind; budget: number; fallback: string; } /** * Expand array fallbacks into ordinary per-paragraph fields: `intro` with * three elements becomes `intro1`, `intro2`, `intro3`, each inheriting the * field's kind and swap. Runs BEFORE hashing and before the wire, in both the * React and the core paths, so the server never learns arrays exist. * Idempotent: expanded specs contain no arrays. */ export declare function expandFieldSpecs(fields: Record): Record; export declare function normalizeFieldSpecs(fields: Record): NormalizedFragmentField[]; /** Strongest provenance across a set of field states (static < cached < generated). */ export declare function strongestFragmentSource(fields: Iterable): ContentSource; /** * Resolve fragments for a declared context. Resolves fast; NEVER rejects; * fallback-shaped on any failure. */ export declare function fetchFragments(options: FetchFragmentsOptions): Promise; export interface CreateClientOptions { /** `st_pk_…` from the dashboard: a signed credential the server requires * on every stream and content request (see publishableKey.ts). Explicit * organizationId/apiBase win over its payload; the key is still sent, * including on the fragment mint POST (see FetchFragmentsOptions). */ publishableKey?: string; organizationId?: string; apiBase?: string; /** Signed mint token (or a function returning one), sent on the mint POST. */ mintToken?: MintTokenSource; fetchImpl?: typeof fetch; } export interface SparkletreeClient { fragments(options: { context?: DeclaredContext; fields: Record; }): Promise; } export interface ResolvedPublishableConfig { organizationId: string; apiBase: string; /** The key as given, carried onto every StreamTarget as `st-key`. */ publishableKey?: string; /** An explicit apiBase is the developer's own choice; a key-derived one * cannot be verified client-side and earns no mint (see maybeMint, S-2). */ apiBaseProvenance: "explicit" | "key"; } /** * The ONE resolution of `{ publishableKey?, organizationId?, apiBase? }`: * explicit props win, the key fills the gaps, and missing configuration or * an insecure apiBase throws at setup time (an apiBase that is neither https * nor loopback http would send org configuration over a channel any middlebox * can rewrite). `createClient` and the React provider both call this — as two * hand-rolled copies they had already diverged once, with the provider * silently accepting a plain-http apiBase that createClient refused. */ export declare function resolvePublishableConfig(options: { publishableKey?: string; organizationId?: string; apiBase?: string; }, caller?: string): ResolvedPublishableConfig; /** * Vanilla entry point, in core where the wire lives. Throws TypeError on a * malformed key or missing configuration — setup errors surface at setup * time; only the resolved `fragments()` call is the never-rejects path. */ export declare function createClient(options: CreateClientOptions): SparkletreeClient; //# sourceMappingURL=fragments.d.ts.map