@foir/sdk 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +497 -27
  2. package/README.md +111 -0
  3. package/dist/auth/auth.d.ts +72 -1
  4. package/dist/auth/hosted.d.ts +71 -4
  5. package/dist/auth/index.d.ts +3 -3
  6. package/dist/auth/operations.d.ts +84 -0
  7. package/dist/auth/ops.d.ts +10 -0
  8. package/dist/auth.js +127 -19
  9. package/dist/client.js +63 -6
  10. package/dist/codegen/block-registry.d.ts +1 -1
  11. package/dist/codegen/block-scaffold.d.ts +1 -1
  12. package/dist/codegen/index.d.ts +1 -1
  13. package/dist/codegen/item-registry.d.ts +1 -1
  14. package/dist/codegen/item-scaffold.d.ts +1 -1
  15. package/dist/content/content-renderer.d.ts +4 -5
  16. package/dist/content/index.d.ts +1 -1
  17. package/dist/content/richtext.d.ts +1 -1
  18. package/dist/data/builtins.d.ts +64 -12
  19. package/dist/data/client.d.ts +82 -1
  20. package/dist/data/decimal.d.ts +212 -0
  21. package/dist/data/document.d.ts +1 -1
  22. package/dist/data/index.d.ts +10 -8
  23. package/dist/data/manifest.d.ts +9 -1
  24. package/dist/data/schema.d.ts +2 -2
  25. package/dist/data/subscriptions.d.ts +36 -10
  26. package/dist/data/transport.d.ts +120 -3
  27. package/dist/data.js +377 -16
  28. package/dist/graphql/cdn-route.d.ts +1 -1
  29. package/dist/graphql/foir-request.d.ts +1 -1
  30. package/dist/live/coalesce.d.ts +57 -0
  31. package/dist/live/drift.d.ts +59 -0
  32. package/dist/live/events.d.ts +13 -0
  33. package/dist/live/headers.d.ts +53 -0
  34. package/dist/live/index.d.ts +54 -0
  35. package/dist/live/keys.d.ts +72 -0
  36. package/dist/live/manager.d.ts +77 -0
  37. package/dist/live/react/index.d.ts +143 -0
  38. package/dist/live/reconcile.d.ts +82 -0
  39. package/dist/live/resync.d.ts +59 -0
  40. package/dist/live/store.d.ts +134 -0
  41. package/dist/live/tagIndex.d.ts +29 -0
  42. package/dist/live/tanstack.d.ts +127 -0
  43. package/dist/live/writetoken.d.ts +86 -0
  44. package/dist/live-react.js +395 -0
  45. package/dist/live.js +845 -0
  46. package/dist/next/cacheTags.d.ts +245 -0
  47. package/dist/next/index.d.ts +7 -0
  48. package/dist/next.js +179 -6
  49. package/dist/push.js +81 -8
  50. package/dist/react.js +76 -5
  51. package/dist/realtime/index.d.ts +4 -5
  52. package/dist/server/events-client.d.ts +54 -4
  53. package/dist/server/index.d.ts +3 -2
  54. package/dist/server/require-scoped-token.d.ts +1 -1
  55. package/dist/server.js +454 -2
  56. package/dist/transport/foir-http.d.ts +113 -10
  57. package/package.json +10 -2
@@ -0,0 +1,59 @@
1
+ import type { FoirRequestError } from '../transport/foir-http';
2
+ /**
3
+ * live/resync.ts — when the cache has to admit it cannot be repaired.
4
+ *
5
+ * # There is no gap to detect, and that is the point
6
+ *
7
+ * The obvious design for "did I miss a message" is a contiguous counter, and it
8
+ * does not work on this stream: `seq` is the project's clock and record changes
9
+ * are only some of what advances it, so two consecutive `recordChanged`
10
+ * messages are LEGITIMATELY non-adjacent. "I missed one" and "nothing else
11
+ * happened" are indistinguishable by counting.
12
+ *
13
+ * A cursor needs only ordering, which `seq` has. So the subscription hands back
14
+ * the highest seq it applied and the server replays what came after it — a drop
15
+ * anywhere upstream is repaired by the next connect rather than noticed by a
16
+ * counter, and there is no gap signal to act on because there is no gap.
17
+ *
18
+ * # What is left is the two refusals
19
+ *
20
+ * The server refuses a cursor it cannot honour rather than replaying part of it,
21
+ * because a cache handed a partial backlog believes it is caught up — which is
22
+ * exactly the silent staleness the cursor removes. Those refusals are the only
23
+ * "your cache is unreliable now" the client ever gets, and each is a HARD
24
+ * RESYNC: throw the cache away and refetch, because there is no way to know what
25
+ * was missed.
26
+ */
27
+ /**
28
+ * `cursor_expired` — the cursor is older than the log's retention window.
29
+ * `backlog_too_large` — the cursor is further behind than the surface will page.
30
+ *
31
+ * Both arrive as a graphql-ws `error` frame with the code in `extensions`, and
32
+ * both are refusals to HANDLE rather than failures to report. Branch on the
33
+ * code, never on the message: the sentence is documentation and will be
34
+ * reworded, the code is the contract.
35
+ */
36
+ export declare const FOIR_RESYNC_ERROR_CODES: readonly ["cursor_expired", "backlog_too_large"];
37
+ export type FoirResyncErrorCode = (typeof FOIR_RESYNC_ERROR_CODES)[number];
38
+ /**
39
+ * Whether a terminal subscription error means the cache must be thrown away.
40
+ *
41
+ * Anything else is a refusal about THIS subscription — a missing scope, an
42
+ * unwatchable model, a transport that gave up — and says nothing about whether
43
+ * what is already cached is still true. Resyncing on those would turn a
44
+ * permission error into a full refetch storm on every reconnect attempt.
45
+ */
46
+ export declare function foirRequiresResync(error: Pick<FoirRequestError, 'code'>): boolean;
47
+ /**
48
+ * The cursor to resume from after a refusal, when the server named one.
49
+ *
50
+ * `cursor_expired` carries `resumeFrom` — one below the oldest event still
51
+ * retained, so resuming from it loses nothing that still exists. Resuming there
52
+ * is only sound AFTER a resync: it skips everything that fell out of the window,
53
+ * and a cache that kept its entries would keep them wrong forever.
54
+ *
55
+ * `backlog_too_large` names no resume position on purpose. The honest recovery
56
+ * is to refetch and start from the seq that read reports, which is a value the
57
+ * client obtains from the response rather than from the refusal.
58
+ */
59
+ export declare function foirResumeCursor(error: Pick<FoirRequestError, 'graphQLErrors'>): number | null;
@@ -0,0 +1,134 @@
1
+ import type { FoirRecordChangedEvent } from '../data/subscriptions';
2
+ import { type FoirTagIndex } from './tagIndex';
3
+ /**
4
+ * live/store.ts — the client's half of the invalidation graph.
5
+ *
6
+ * # IT STORES THE RAW WIRE PAYLOAD, and hydration happens at the read boundary
7
+ *
8
+ * This is the load-bearing decision in the file and it is forced, not chosen.
9
+ * `FoirDecimal` is a class with `#`-private fields, and every ordinary cache
10
+ * reflex breaks on it QUIETLY:
11
+ *
12
+ * - `Object.keys(d)` is `[]`. A structural deep-equal walking own enumerable
13
+ * properties sees `{}` for every decimal, so it reports `decimal('1.00')`
14
+ * equal to `decimal('999.99')`. That is a false NEGATIVE on change
15
+ * detection — the cache serves a stale value and never learns otherwise,
16
+ * which is worse than a false positive by every measure.
17
+ * - `structuredClone(d)` returns `{}` and DOES NOT THROW. The standard deep
18
+ * clone a cache reaches for destroys the value in silence.
19
+ * - `[Symbol.toPrimitive]` throws on the number and default hints, so any
20
+ * comparison doing `a < b` or `Number(a)` for ordering is a `TypeError`.
21
+ * - `toJSON()` emits the canonical string and `JSON.parse` does not revive it,
22
+ * so persistence is asymmetric by type.
23
+ * - `equals()` is scale-insensitive while `toString()` is scale-preserving, so
24
+ * even a correct comparison cannot be reference- or text-based.
25
+ *
26
+ * Raw wire JSON has none of those properties, because a decimal there is just a
27
+ * string. Change detection becomes an exact string compare, persistence is
28
+ * correct by construction (persist raw, rehydrate through the same path as a
29
+ * fresh response), and the completeness mask operates on the raw keys `omit`
30
+ * already operates on.
31
+ *
32
+ * So: **never cache hydrated entities.** There is no `structuredClone` and no
33
+ * generic deep-equal anywhere below, and their absence is the design rather than
34
+ * an omission.
35
+ *
36
+ * # Keys normalise, payloads never do
37
+ *
38
+ * The converse hazard, and the more expensive one. Dedupe entities by value and
39
+ * keep the first instance and you serve `"1.0"` where the server sent `"1.00"` —
40
+ * a silent change to how a price renders, because trailing zeros are significant
41
+ * for money. The store never holds a decimal instance at all, so this half falls
42
+ * out for free. `live/keys.ts` does the normalising, on keys only.
43
+ */
44
+ /** One cached response, exactly as it came off the wire. */
45
+ export interface FoirLiveEntry {
46
+ /**
47
+ * The RAW payload. Not hydrated, not cloned, not normalised — see the file
48
+ * comment. Treat as immutable: it is handed out by reference, and a consumer
49
+ * that mutates it corrupts every future read of this key.
50
+ */
51
+ payload: unknown;
52
+ /** The response's dependency set, as the server computed it. */
53
+ tags: readonly string[];
54
+ /** The project seq this response resolved under, or `null` for unknown. */
55
+ seq: number | null;
56
+ /**
57
+ * The fields the call asked the server to leave out. The COMPLETENESS MASK.
58
+ * Empty means the entry is whole.
59
+ */
60
+ omitted: readonly string[];
61
+ /** When it was written, for a consumer applying its own staleness floor. */
62
+ storedAt: number;
63
+ }
64
+ export interface FoirLiveWrite {
65
+ payload: unknown;
66
+ tags?: readonly string[];
67
+ seq?: number | null;
68
+ /** The call's `omit`, verbatim. */
69
+ omit?: readonly string[];
70
+ }
71
+ export interface FoirLiveReadOptions {
72
+ /**
73
+ * Fields this read needs present. An entry that declares any of them omitted
74
+ * is a MISS rather than a partial answer.
75
+ */
76
+ needs?: readonly string[];
77
+ }
78
+ /** What changed, so a consumer can act on it without diffing the store. */
79
+ export interface FoirLiveInvalidation {
80
+ /** The keys dropped by this event. */
81
+ keys: string[];
82
+ /** The tags the event resolved to. */
83
+ tags: string[];
84
+ /** True when the event was ignored as already applied. */
85
+ ignored: boolean;
86
+ }
87
+ /**
88
+ * The write half of one in-flight request, handed to the function that issues
89
+ * it.
90
+ *
91
+ * It exists because the fence CANNOT be established at write time. A write that
92
+ * belongs to a request issued before a resync has to be recognisable as such
93
+ * when it lands, and by then the store looks exactly as it does for a fresh one
94
+ * — so the moment that has to be captured is the moment the request STARTS.
95
+ * Handing the fence to the running function is what makes that unforgettable:
96
+ * the natural way to store a response is through the object you were given.
97
+ */
98
+ export interface FoirWriteFence {
99
+ /** Write this request's response, unless the store has resynced since. */
100
+ write(write: FoirLiveWrite): boolean;
101
+ /** Whether this request is still current. */
102
+ readonly current: boolean;
103
+ }
104
+ export interface FoirLiveStore {
105
+ /** Read an entry, honouring the completeness mask. */
106
+ read(key: string, options?: FoirLiveReadOptions): FoirLiveEntry | undefined;
107
+ /**
108
+ * Write a response, UNFENCED. Refuses a result older than the one already
109
+ * applied, but knows nothing about resyncs — use the fence `dedupe` hands the
110
+ * running function for anything that came off the network.
111
+ */
112
+ write(key: string, write: FoirLiveWrite): boolean;
113
+ /** Drop one key. */
114
+ drop(key: string): void;
115
+ /**
116
+ * Share one in-flight request between concurrent callers of the same key, and
117
+ * hand the runner a fence bound to this request's epoch.
118
+ */
119
+ dedupe<T>(key: string, run: (fence: FoirWriteFence) => Promise<T>): Promise<T>;
120
+ /** Apply a record change: drop everything that depended on it. */
121
+ applyRecordChange(event: FoirRecordChangedEvent): FoirLiveInvalidation;
122
+ /** Throw everything away, and fence anything already in flight. */
123
+ resync(): void;
124
+ /** Called after every invalidation and every resync. */
125
+ onInvalidate(listener: (invalidation: FoirLiveInvalidation) => void): () => void;
126
+ readonly size: number;
127
+ /** Test seam and introspection: the graph itself. */
128
+ readonly tags: FoirTagIndex;
129
+ }
130
+ export interface FoirLiveStoreOptions {
131
+ /** Clock, injectable so a test can pin `storedAt`. */
132
+ now?: () => number;
133
+ }
134
+ export declare function createFoirLiveStore(options?: FoirLiveStoreOptions): FoirLiveStore;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * live/tagIndex.ts — the dependency graph, both ways round.
3
+ *
4
+ * The server already knows which records and collections every response depends
5
+ * on; it has to, to invalidate its own cache. This is that graph held on the
6
+ * client: `key → tags` so an entry can be un-indexed when it is dropped, and
7
+ * `tag → keys` so an event can name what it invalidates without walking the
8
+ * store.
9
+ *
10
+ * Both directions, because keeping only one turns the other question into a
11
+ * linear scan of everything cached — and the read side runs on every response
12
+ * while the write side runs on every event. It is the same reason a database
13
+ * keeps an index rather than sorting on demand.
14
+ */
15
+ export interface FoirTagIndex {
16
+ /** Replace the tag set for one key. */
17
+ set(key: string, tags: readonly string[]): void;
18
+ /** Forget a key entirely. */
19
+ remove(key: string): void;
20
+ /** The tags recorded for a key. */
21
+ tagsFor(key: string): readonly string[];
22
+ /** Every key that carries any of `tags`, deduped. */
23
+ keysForTags(tags: readonly string[]): string[];
24
+ /** Forget everything. */
25
+ clear(): void;
26
+ /** How many keys are indexed. */
27
+ readonly size: number;
28
+ }
29
+ export declare function createFoirTagIndex(): FoirTagIndex;
@@ -0,0 +1,127 @@
1
+ import type { FoirRecordChangedEvent } from '../data/subscriptions';
2
+ import { type FoirLiveCall } from './keys';
3
+ /**
4
+ * live/tanstack.ts — the adapter, and it is deliberately an adapter.
5
+ *
6
+ * Users keep the query library they already have. What is exported here is the
7
+ * query-key derivation and the rule for what a record change does to loaded
8
+ * pages; we own nothing of the rendering layer and no part of the cache.
9
+ *
10
+ * The differentiation is entirely in the invalidation graph, and an adapter
11
+ * captures all of it. A store of our own would add a permanent React support
12
+ * surface for no differentiated gain.
13
+ *
14
+ * ## Structurally typed, so `@tanstack/react-query` is not a dependency
15
+ *
16
+ * The three methods below are the whole of what this needs from a query client.
17
+ * A real `QueryClient` satisfies the interface, and the SDK neither imports nor
18
+ * pins a version of it — the same reason the Next adapter takes `cacheTag` as a
19
+ * function.
20
+ */
21
+ export interface FoirQueryClientLike {
22
+ invalidateQueries(filters: {
23
+ queryKey: readonly unknown[];
24
+ exact?: boolean;
25
+ }): unknown;
26
+ getQueriesData(filters: {
27
+ queryKey: readonly unknown[];
28
+ }): Array<[readonly unknown[], unknown]>;
29
+ setQueriesData(filters: {
30
+ queryKey: readonly unknown[];
31
+ }, updater: (previous: unknown) => unknown): unknown;
32
+ }
33
+ /** The root every Foir query key starts with, so a filter can reach all of them. */
34
+ export declare const FOIR_QUERY_KEY_ROOT: "foir";
35
+ /**
36
+ * The query key for one call.
37
+ *
38
+ * An ARRAY, because that is what a query library filters on hierarchically:
39
+ * `['foir']` reaches every Foir query, `['foir', 'product']` reaches one model's,
40
+ * and the full key reaches one call. The last segment is `foirQueryKey`'s
41
+ * string, which is where the real work is — stable ordering, `undefined` kept
42
+ * distinct from absent, and decimals canonicalised so `1.0` and `1.00` are one
43
+ * entry rather than two.
44
+ */
45
+ export declare function foirQueryKeys(call: FoirLiveCall): readonly unknown[];
46
+ /** Every query for one model. */
47
+ export declare function foirModelQueryKey(modelKey: string): readonly unknown[];
48
+ /**
49
+ * How live a query is.
50
+ *
51
+ * `auto` — apply the full rule below.
52
+ * `notify` — never touch the data; report that something changed and let the UI
53
+ * decide. The honest setting for a reader mid-scroll.
54
+ * `off` — ignore the stream entirely.
55
+ */
56
+ export type FoirLiveMode = 'auto' | 'notify' | 'off';
57
+ /** What one record change did, so a UI can render the parts it cannot do itself. */
58
+ export interface FoirLiveEffect {
59
+ /** Queries whose data was refetched (an `updated` record they hold). */
60
+ invalidated: ReadonlyArray<readonly unknown[]>;
61
+ /** Queries a row was removed from without any fetch (a `deleted` record). */
62
+ removed: ReadonlyArray<readonly unknown[]>;
63
+ /**
64
+ * The model gained a record. NOT applied to any query — see below for why a
65
+ * create cannot be spliced — so a UI shows "N new" and refetches on a click.
66
+ */
67
+ created: boolean;
68
+ /** `totalCount` on this model's loaded queries is now approximate. */
69
+ totalCountStale: boolean;
70
+ }
71
+ export interface FoirApplyOptions {
72
+ mode?: FoirLiveMode;
73
+ /**
74
+ * The field a record's id arrives under. `_id` is the platform's own
75
+ * spelling and is what the generated types emit.
76
+ */
77
+ idField?: string;
78
+ /**
79
+ * Where an `updated` invalidation goes. Defaults to calling the query client
80
+ * straight away.
81
+ *
82
+ * The seam exists so a burst can be COALESCED: fifty writes on one model fire
83
+ * fifty invalidations, and refetching each one turns the exact screen a burst
84
+ * is most likely to be watched from into nothing but refetching. The
85
+ * coalescer takes the key here and runs one refetch per key per window.
86
+ *
87
+ * Only the update path is routed through it. A delete is applied in memory
88
+ * with no fetch at all, and a create deliberately fetches nothing — neither
89
+ * has anything to coalesce.
90
+ */
91
+ invalidate?: (queryKey: readonly unknown[]) => void;
92
+ }
93
+ /**
94
+ * Apply one record change to the loaded queries for its model.
95
+ *
96
+ * # Pagination, decided — and the three cases are handled ASYMMETRICALLY
97
+ * # because that is what the data supports
98
+ *
99
+ * Foir is pure Relay cursor pagination with no `offset` or `skip`, so loaded
100
+ * pages keep their relative order and `endCursor` stays correct. Only three
101
+ * things actually break:
102
+ *
103
+ * - **`deleted` → remove the row by id, with NO FETCH.** The event carries
104
+ * the id, so this is instantly correct and costs a round trip nothing.
105
+ * - **`created` → expose a COUNT, do not refetch.** A create cannot be
106
+ * spliced: record events carry no body, and even with one you could not
107
+ * place the row without knowing the query's sort. Refetching the head page
108
+ * instead was considered and rejected — it prepends under a reader
109
+ * mid-scroll, which is the exact disruption this avoids.
110
+ * - **`updated` → refetch the queries holding that record.** Membership is
111
+ * unchanged, so the cursors hold.
112
+ *
113
+ * `totalCount` goes stale on any membership change and is flagged rather than
114
+ * corrected: correcting it would need a refetch, which is the thing being
115
+ * avoided.
116
+ *
117
+ * # Mutable sort keys are a DOCUMENTED LIMITATION, not a bug to fix
118
+ *
119
+ * A query ordered by `updatedAt`, or by a `position` a user drags, can have a
120
+ * record move between pages on update — appearing twice, or vanishing. There is
121
+ * no detection here and there should not be: it needs client-side knowledge of
122
+ * the sort fields, and the fully-correct alternative turns a routine edit into
123
+ * an N-page refetch on most feeds. Say it in the docs; do not build for it.
124
+ */
125
+ export declare function applyFoirRecordChange(queryClient: FoirQueryClientLike, event: FoirRecordChangedEvent, options?: FoirApplyOptions): FoirLiveEffect;
126
+ /** The tags one change would invalidate, for a consumer driving its own cache. */
127
+ export declare function foirTagsForQueryInvalidation(event: FoirRecordChangedEvent): string[];
@@ -0,0 +1,86 @@
1
+ import type { FoirHeaderSource } from './headers';
2
+ /**
3
+ * live/writetoken.ts — read your own write without throwing the cache away.
4
+ *
5
+ * # The problem it replaces
6
+ *
7
+ * The documented answer for reading your own write used to be
8
+ * `Cache-Control: no-cache`: skip the entry, resolve fresh, repopulate. It
9
+ * works, and it discards every warm entry on that path — so a storefront that
10
+ * does one write makes the next visitor pay a cold read on that query.
11
+ *
12
+ * A floor is the precise version. The mutation hands back a token naming where
13
+ * it committed; a later read carries it; the server serves a cached entry only
14
+ * if that entry resolved at or above the write's position, and resolves fresh
15
+ * otherwise. Same guarantee, and entries that are already fresh enough survive.
16
+ *
17
+ * # The token is opaque, and it is not a seq
18
+ *
19
+ * It names an EVENT, not a position, and the difference is why the mutation does
20
+ * not have to wait. `seq` is stamped asynchronously, by a different process on a
21
+ * different tick, so a mutation that waited for its own stamp could not complete
22
+ * unless a background loop elsewhere was alive and current — trading a
23
+ * permanently reliable write for a sometimes-faster read.
24
+ *
25
+ * So the read path resolves the token instead, and degrades correctly when it
26
+ * cannot: an unstamped event states no ordering yet, so the read simply resolves
27
+ * fresh, which is exactly the old `no-cache` behaviour for the tens of
28
+ * milliseconds before the stamp lands. Nothing here should try to parse it.
29
+ *
30
+ * # Why this is a primitive rather than automatic
31
+ *
32
+ * Holding "the last write token" is application state — which write, for how
33
+ * long, and across which of a user's tabs are questions this package cannot
34
+ * answer, and guessing produces either a floor that expires too early to help or
35
+ * one that pins reads to origin long after the write stopped mattering.
36
+ *
37
+ * So the SDK names the header and reads it; the consumer decides the lifetime.
38
+ * `readFoirLiveHeaders` takes the same line for the same reason.
39
+ */
40
+ /**
41
+ * The header carrying the write token, in BOTH directions: out of a mutation's
42
+ * response, back in on a read's request.
43
+ *
44
+ * One name rather than a pair, because it is one value making a round trip and
45
+ * two names would be two things to keep in step.
46
+ *
47
+ * A header rather than a schema field, deliberately: a field would pull in
48
+ * foir-proto, a regen, the sync-gen job and the vocabulary mirrors, for a value
49
+ * that is transport bookkeeping and never part of anyone's data model.
50
+ */
51
+ export declare const FOIR_WRITE_TOKEN_HEADER = "X-Foir-Write-Token";
52
+ /**
53
+ * The write token a mutation's response carried, or `null`.
54
+ *
55
+ * Absent is the ordinary case for anything that did not write, and for a write
56
+ * whose emit produced no event. `null` means "no floor to state", never "floor
57
+ * of zero" — a zero floor would be satisfied by every cached entry ever, which
58
+ * is silently the opposite of what a caller asking for one wants.
59
+ *
60
+ * # It is readable from a browser, and that is not accidental
61
+ *
62
+ * A response header is invisible to cross-origin JS unless the server lists it
63
+ * in `Access-Control-Expose-Headers`, and api-public lists this one — alongside
64
+ * the matching entry on the ALLOWED REQUEST headers, so the token can also be
65
+ * sent back. Both halves are needed and the request half is the one that is easy
66
+ * to forget, because its absence is a preflight failure that names the header
67
+ * and not the file.
68
+ *
69
+ * What can still take it away is a proxy of the consumer's own sitting in front
70
+ * of the API. Stripped from the response, this returns `null` and looks exactly
71
+ * like a mutation that emitted nothing; stripped from the request, the floor is
72
+ * silently never applied.
73
+ */
74
+ export declare function readFoirWriteToken(headers: FoirHeaderSource): string | null;
75
+ /**
76
+ * The request headers a floored read should carry, or an empty object.
77
+ *
78
+ * Empty rather than a header with an empty value, because the two are different
79
+ * requests: a header present but blank is a floor the server has to interpret,
80
+ * and the honest way to say "no floor" is to say nothing.
81
+ *
82
+ * Sending this also changes the LANE — see `foirRoute`, which reports `floor`
83
+ * for a token-bearing read. A floor the shared edge cannot honour is worse than
84
+ * no floor at all, because it reads as satisfied.
85
+ */
86
+ export declare function foirWriteTokenHeaders(token: string | null | undefined): Record<string, string>;