@jokerized/decksmith 0.5.2 → 0.6.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.
@@ -105,6 +105,46 @@ interface Frame {
105
105
  * cannot drive it. `present()` says so out loud rather than rendering blank.
106
106
  */
107
107
  export declare function frameOf(player: Player): Frame | null;
108
+ /**
109
+ * Whether a scene is on screen at composition time `t`: from its slide's start
110
+ * until its CLIP ends, which is the engine's own rule — hyperframes shows a clip
111
+ * while `start <= t < start + data-duration`, and nothing else.
112
+ *
113
+ * THE CLIP, NOT THE SLIDE, and the slide was a live blink. `composition.ts`
114
+ * lets every scene but the last outlast its slide by one handoff, so the
115
+ * outgoing scene dissolves over the incoming one's empty opening — `chromeIn`
116
+ * puts first ink at 0.15s. The render honoured that; this function used the
117
+ * island's `endTime`, hid the outgoing scene on the seam's instant, and a glide
118
+ * across a seam cut a lit slide to flat background. Measured 2026-09-18 on
119
+ * deck.html at hyperframes 0.8.43 with a hand-stepped 60Hz clock, gliding
120
+ * 6.2s -> 8.55s over a `stack` -> `pipeline` seam at 7.0s: ELEVEN ticks,
121
+ * 7.000s through 7.167s, of background and presenter chrome alone, while the
122
+ * mp4 of the same deck had no background-only frame at any seam. See
123
+ * `.planning/2026-09-18-scene-boundary-blink.md`.
124
+ *
125
+ * `clip` is the scene div's `data-duration`. Anything unusable falls back to the
126
+ * island's window, which is what every deck did before the handoff existed.
127
+ */
128
+ export declare function showingAt(slide: SlideSpec, clip: number, t: number): boolean;
129
+ /**
130
+ * Put the composition on the frame at time `t`.
131
+ *
132
+ * `player.seek()` moves the player's own clock; under the render engine that is
133
+ * enough, because the engine drives each scene's timeline and clip visibility
134
+ * itself. The standalone player bundle does neither — so a seeked deck shows
135
+ * every scene stacked with all its entrance tweens still at their `from` state,
136
+ * i.e. blank. Scenes are addressed by the ids the island already carries; the
137
+ * one thing read off the scene itself is how long its clip runs, because the
138
+ * island only knows the slide (see `showingAt`).
139
+ *
140
+ * Exported so test/deck.test.ts can drive it through a fake frame. Testing
141
+ * `showingAt` alone left this function free to stop calling it: put main's
142
+ * slide-window rule back here and every deck test passed, the Chrome suite
143
+ * included, because test/deck-page.test.ts only checks resting stops and no
144
+ * stop falls inside a handoff. The deck-runtime bundle is an IIFE, so the
145
+ * export does not reach deck.html.
146
+ */
147
+ export declare function paint(frame: Frame, slides: readonly SlideSpec[], t: number): void;
108
148
  export interface Voice {
109
149
  /**
110
150
  * Arrive at a stop: cut whatever was speaking, start this stop from zero.
@@ -70,6 +70,14 @@
70
70
  * lives on the clip and nowhere else. The 150ms proposed here turned out to
71
71
  * be short by half; `HANDOFF_SECONDS` carries the luma trace that fixes it.
72
72
  *
73
+ * IT CLOSED IN THE RENDER AND NOT IN deck.html, until 2026-09-18. The engine
74
+ * reads the clip; `paint()` in `src/deck/runtime.ts` read the island's slide
75
+ * window, so a presented deck gliding across a seam still hid the outgoing
76
+ * scene on the seam's instant — eleven 60Hz ticks of flat background on a
77
+ * `stack`→`pipeline` seam at hyperframes 0.8.43, while the mp4 of the same
78
+ * deck had none at any of its three seams. `showingAt` now gives `paint()`
79
+ * the engine's rule. `.planning/2026-09-18-scene-boundary-blink.md`.
80
+ *
73
81
  * That dip is a compromise and it is worth naming. The spike's version keeps
74
82
  * the container lit while the contained scene arrives inside it, which needs
75
83
  * both scenes on screen at once — and `hyperframes lint` rejects that
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import type { z } from "zod";
14
14
  import { type Cut } from "../plan/select.js";
15
- import type { Format, Source, Storyboard, segmentSchema } from "../types.js";
15
+ import type { Format, NarrationCanvas, Source, Storyboard, segmentSchema } from "../types.js";
16
16
  import { type Scene } from "./kit.js";
17
17
  /** A built deck. `page` is present only for navigable formats. */
18
18
  export interface Deck {
@@ -39,6 +39,14 @@ export interface DeckNarration {
39
39
  voice: string;
40
40
  /** Directory holding the mp3s, relative to `deck.html`. */
41
41
  dir: string;
42
+ /**
43
+ * What `narrate` split the sentences over: the canvas, each narrated beat's
44
+ * stop count, and the density cap. Absent on narration written before they
45
+ * were recorded, which builds unchecked. See `assertNarrationStaging`.
46
+ */
47
+ canvas?: NarrationCanvas | undefined;
48
+ stops?: Record<string, number> | undefined;
49
+ speakingStops?: number | undefined;
42
50
  beats: Record<string, Segment[]>;
43
51
  }
44
52
  /**
@@ -1,20 +1,24 @@
1
+ import type { Format, Storyboard } from "../types.js";
2
+ import { type Scene } from "./kit.js";
3
+ import { type DeckTheme } from "./themes/index.js";
4
+ export { type DeckTheme, ink, mono, paper, resolveTheme, THEME_NAMES, THEMES, } from "./themes/index.js";
1
5
  /**
2
- * The shared stylesheet, the ambient gate, and the deck's pacing.
6
+ * The theme a storyboard's scenes are staged with, and the font family it ships.
3
7
  *
4
- * The palettes themselves moved to `./themes/` once there was more than one;
5
- * this file stays the door to them, so nothing that imports `ambient` or `ink`
6
- * had to be touched to gain a registry.
8
+ * ONE RESOLUTION for every place that emits a beat to learn how it stages:
9
+ * `planCut` and `layout`, `planTiming`, and `narrate`. It is not only colour.
10
+ * The family goes first in `fontStack`, `faceOf` reads it, and a Hangul or CJK
11
+ * face measures Latin runs wider, so a beat can fit in one theme and be refused
12
+ * in the other. MEASURED 2026-09-18: `narrate` staged with the bare `ink` theme,
13
+ * and a `lang: "ko"` callout at 1380×776 was one stop there and four in the
14
+ * build, so its three sentences played as one segment over the first card.
7
15
  *
8
- * What is left here is everything a theme does NOT decide per-theme: the reset,
9
- * the scene box, the two ambient keyframes, and `pace`. Typography belongs to
10
- * the archetype that draws it, not to this file. Two stylesheets describing one
11
- * `.figwrap` is how a rule ends up being decided by emission order, so nothing
12
- * an emitter styles is styled here as well.
16
+ * `name` overrides `storyboard.theme`, as `--theme` does at build.
13
17
  */
14
- import type { Format } from "../types.js";
15
- import { type Scene } from "./kit.js";
16
- import type { DeckTheme } from "./themes/index.js";
17
- export { type DeckTheme, ink, mono, paper, resolveTheme, THEME_NAMES, THEMES, } from "./themes/index.js";
18
+ export declare function deckLook(storyboard: Pick<Storyboard, "theme" | "lang">, name?: string): {
19
+ family: string | null;
20
+ theme: DeckTheme;
21
+ };
18
22
  /** Where `ingest` writes the subsetted font bundle, relative to the deck. */
19
23
  export declare const FONT_BUNDLE_DIR = "assets/fonts";
20
24
  export declare const FONT_BUNDLE_HREF = "assets/fonts/fonts.css";
@@ -14,13 +14,17 @@
14
14
  * archetype whose reveal count depends on its params.
15
15
  */
16
16
  import type { z } from "zod";
17
+ import type { Theme } from "../emit/kit.js";
17
18
  import type { Source, Storyboard } from "../types.js";
18
- import { type Beat, type Format, type prefsSchema, type segmentSchema } from "../types.js";
19
+ import { type Beat, type Format, type NarrationCanvas, type prefsSchema, type segmentSchema } from "../types.js";
19
20
  import { type Runner } from "./tts.js";
20
21
  type Prefs = z.infer<typeof prefsSchema>;
21
22
  type Segment = z.infer<typeof segmentSchema>;
22
23
  type Narration = {
23
24
  voice: string;
25
+ canvas: NarrationCanvas;
26
+ stops: Record<string, number>;
27
+ speakingStops?: number;
24
28
  beats: Record<string, Segment[]>;
25
29
  };
26
30
  /**
@@ -41,8 +45,13 @@ export declare function stopCount(holds: readonly number[]): number;
41
45
  * `onBeatError`, which is where a missing slide is reported and where a caller
42
46
  * without that hook still gets the error. Throwing here instead killed the whole
43
47
  * job one stage early, with the hook the caller had passed never reached.
48
+ *
49
+ * `theme` must be the one the build stages with, `deckLook(storyboard).theme`,
50
+ * which is what `narrate` passes: the font face it names decides whether a beat
51
+ * fits. The `ink` default is for a caller that has no storyboard and no
52
+ * CJK-language beat.
44
53
  */
45
- export declare function stopsFor(beat: Beat, source: Source, format: Format, sid?: string): number;
54
+ export declare function stopsFor(beat: Beat, source: Source, format: Format, sid?: string, theme?: Theme): number;
46
55
  /**
47
56
  * Split narration into sentences.
48
57
  *
@@ -65,12 +74,26 @@ export declare function splitSentences(text: string): string[];
65
74
  * Returns one entry per stop; `""` means nothing is said there.
66
75
  */
67
76
  export declare function planSegments(text: string, stops: number): string[];
77
+ /**
78
+ * How many stops `planSegments` gives words to, for a beat staged with `stops`
79
+ * stops under a density cap of `cap`.
80
+ *
81
+ * Two stagings that answer the same number split `text` identically, sentence
82
+ * for sentence and stop for stop: the first n-1 speaking stops get one sentence
83
+ * each and the last gets the rest. So this, not the raw stop count, is what has
84
+ * to agree between `narrate` and a build. A beat with three sentences reads the
85
+ * same over four stops as over five; the extra stop is silent either way.
86
+ */
87
+ export declare function speakingStopCount(text: string, stops: number, cap?: number): number;
68
88
  export interface NarrateOpts {
69
89
  /** Directory the audio is written into. Returned paths are relative to it. */
70
90
  dir: string;
71
91
  /** Injected in tests so no test reaches the network. */
72
92
  runner?: Runner;
73
- /** Staging differs by canvas, so the stop count does too. */
93
+ /**
94
+ * Staging differs by canvas, so the stop count does too. Recorded in the
95
+ * result as `canvas`, which is what lets `build` refuse another one.
96
+ */
74
97
  format?: Format;
75
98
  }
76
99
  /**
@@ -84,4 +107,51 @@ export interface NarrateOpts {
84
107
  * actually speak.
85
108
  */
86
109
  export declare function narrate(storyboard: Storyboard, source: Source, prefs: Prefs, opts: NarrateOpts): Promise<Narration>;
110
+ /** The part of a narration a build checks, as `narrate` wrote it. */
111
+ export interface NarrationStaging {
112
+ canvas?: NarrationCanvas | undefined;
113
+ stops?: Readonly<Record<string, number>> | undefined;
114
+ speakingStops?: number | undefined;
115
+ beats: Readonly<Record<string, readonly {
116
+ text: string;
117
+ }[]>>;
118
+ }
119
+ /** What `narrate` records about the format it staged against. */
120
+ export declare function narrationCanvas(format: Format): NarrationCanvas;
121
+ /**
122
+ * The warning for narration that records no stop counts, or undefined.
123
+ *
124
+ * ACCEPTED, NOT REFUSED. That is every `narration.json` and every pack written
125
+ * before `narrate` recorded them. Refusing would make each of those decks
126
+ * unbuildable until re-narrated, and a pack carries the mp3s but not the TTS
127
+ * cache sidecars, so for an unpacked deck that is every sentence synthesised
128
+ * again. The cost is that an old file staged differently still builds wrong,
129
+ * and this sentence is the only thing that says it was not checked. The library
130
+ * has no channel for it; the CLI prints it.
131
+ */
132
+ export declare function uncheckedNarration(narration: NarrationStaging, format: Format): string | undefined;
133
+ /**
134
+ * Refuse narration whose sentences were split over stops this build does not
135
+ * stage.
136
+ *
137
+ * `staged` is each beat THIS DECK DRAWS mapped to its stop count here: a beat
138
+ * the build leaves out, by refusal or by budget, speaks nowhere and cannot be
139
+ * wrong. For each one that has segments, the speaking-stop count `narrate` split
140
+ * against is compared with the one this staging gives (`speakingStopCount`).
141
+ *
142
+ * COMPARED PER BEAT, NOT BY CANVAS. The canvas was the first version of this
143
+ * check and it was wrong both ways: it refused `deck-16x9` narration built at
144
+ * `short-9x16`, which stages every drawn demo beat identically, and it passed a
145
+ * Korean deck whose font face made `narrate` and `build` stage one beat
146
+ * differently at the same size. A beat's own stop count is the fact both sides
147
+ * record, so it also catches a params edit that moves the count without
148
+ * changing a word, which `scanNarrationDrift` cannot see.
149
+ *
150
+ * THROWN, NOT REPORTED, for the reason `scanNarrationDrift` is an error: the
151
+ * two sides are recorded facts that must agree, and what gets through is a deck
152
+ * whose voice describes reveals that are not on screen. `place` clamps a stop
153
+ * past the end, `assertFits` skips a clamped one, and `build` printed PASS on
154
+ * the measured case.
155
+ */
156
+ export declare function assertNarrationStaging(narration: NarrationStaging, staged: ReadonlyMap<string, number>, format: Format): void;
87
157
  export {};
@@ -163,9 +163,11 @@ export declare function assertHoldsAgree(scenes: TimedScene[], fragments: Record
163
163
  * timelines and why the video keeps its length.
164
164
  *
165
165
  * A segment whose `stop` is past the end of the stop list is clamped onto the
166
- * last one. That only happens when a deck is narrated for one format and built
167
- * for another whose staging differs; speaking it late over the final state
168
- * beats dropping the sentence on the floor.
166
+ * last one. That happens only when the staging moved after `narrate` ran and
167
+ * the narration records no stop counts to say so: a file written before they
168
+ * were recorded. One that records them never gets here with a moved beat;
169
+ * `assertNarrationStaging` refuses it first. Speaking it late over the final
170
+ * state beats dropping the sentence on the floor.
169
171
  */
170
172
  export declare function place(scenes: TimedScene[], spoken: Record<string, Segment[]>): TimedSegment[];
171
173
  /**
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Who may use the server, and over what: the startup policy, the token, and the
3
+ * browser session cookie.
4
+ *
5
+ * NODE BUILTINS ONLY. `build:server` transpiles this directory file by file
6
+ * without bundling, so an import of anything else would survive into
7
+ * dist/server/ and resolve to nothing — the failure "the server's imports
8
+ * survive the build" in test/server.test.ts exists to catch.
9
+ *
10
+ * THE POLICY IS REFUSE, NEVER WARN. A server that starts with a warning about
11
+ * being open is a server that is open: nobody reads a banner. So every
12
+ * configuration that would put the Codex spend endpoint on a network without a
13
+ * token and a certificate, or would quietly ignore a credential someone set, is
14
+ * a refusal at startup that names each problem and what fixes it. There is no
15
+ * override flag, and `createDeckServer` repeats the one check that matters most
16
+ * so no future caller can skip this file.
17
+ *
18
+ * WHAT IS KEPT IN MEMORY is `sha256(token)` to compare against and an HKDF of
19
+ * the token to sign sessions with. The token itself, a cookie value and an
20
+ * `Authorization` header are never logged, and no message here prints a file's
21
+ * contents or a fingerprint of the token: an unsalted hash prefix in a log is an
22
+ * offline oracle for a weak one.
23
+ *
24
+ * A SESSION BELONGS TO ONE LISTENER, not to the host. The port is in the cookie's
25
+ * name and `host:port` is inside the MAC, because cookies have no port and two
26
+ * DeckSmiths reading one token file would otherwise be interchangeable to a
27
+ * browser. That is a collision fix, NOT confidentiality: the browser still hands
28
+ * this cookie to every listener on the host, and the README says what that lets a
29
+ * local account do.
30
+ */
31
+ import { X509Certificate } from "node:crypto";
32
+ /**
33
+ * A loopback bind, spelled exactly as `DECKSMITH_HOST` must spell it. Anything
34
+ * else — `127.0.0.2`, `::ffff:127.0.0.1`, `0.0.0.0`, an empty string — is
35
+ * treated as exposed. A list that fails closed on a spelling it does not know is
36
+ * the point; resolving names here would make the answer depend on DNS.
37
+ */
38
+ export declare const LOOPBACK_BINDS: string[];
39
+ /** How long a browser session lasts. A constant, not a setting. */
40
+ export declare const SESSION_SECONDS: number;
41
+ /**
42
+ * The floor `createDeckServer` gives OpenSSL, and the floor `readTls` validates
43
+ * against. ONE CONSTANT because the two must not drift: validating a pair under
44
+ * a laxer policy than the server will use is a refusal that never fires and a
45
+ * stack trace at listen instead.
46
+ */
47
+ export declare const TLS_MIN_VERSION: "TLSv1.2";
48
+ /** What a request is compared against. Derived from the token, never the token. */
49
+ export interface AuthKeys {
50
+ tokenHash: Buffer;
51
+ sessionKey: Buffer;
52
+ }
53
+ export interface TlsMaterial {
54
+ /** The PEM chain, leaf first, exactly as read. */
55
+ cert: Buffer;
56
+ key: Buffer;
57
+ /** The leaf. Its SANs are the Host allowlist on an exposed bind. */
58
+ x509: X509Certificate;
59
+ }
60
+ export interface Security {
61
+ host: string;
62
+ loopback: boolean;
63
+ sandboxDecks: boolean;
64
+ auth?: AuthKeys & {
65
+ file: string;
66
+ };
67
+ tls?: TlsMaterial & {
68
+ certPath: string;
69
+ keyPath: string;
70
+ };
71
+ /** Said once at startup; never a reason to refuse. */
72
+ warnings: string[];
73
+ }
74
+ export interface SecurityDeps {
75
+ readFile: (path: string) => Buffer;
76
+ stat: (path: string) => {
77
+ mode: number;
78
+ isFile(): boolean;
79
+ };
80
+ now: () => number;
81
+ platform: NodeJS.Platform;
82
+ }
83
+ /** Every problem at once, so fixing a configuration is one restart and not five. */
84
+ export declare class SecurityRefusal extends Error {
85
+ readonly problems: string[];
86
+ constructor(problems: string[]);
87
+ }
88
+ /**
89
+ * Read the environment into a server configuration, or refuse.
90
+ *
91
+ * Pure apart from `deps`, which tests replace. `main.ts` calls it before it
92
+ * creates the work directory or sweeps it, so a refused start leaves nothing on
93
+ * disk.
94
+ */
95
+ export declare function resolveSecurity(env: NodeJS.ProcessEnv, deps?: SecurityDeps): Security;
96
+ /**
97
+ * The lines `main.ts` prints about how the server is reached. Never the token.
98
+ *
99
+ * THE URL MUST BE ONE THIS SERVER ANSWERS TO. On an exposed bind the `Host`
100
+ * allowlist is the certificate's SANs and nothing else, and the bind address is
101
+ * almost never one of them — `https://0.0.0.0:8475`, which this banner used to
102
+ * print, is a URL the server refuses with 403. So the exposed banner prints a
103
+ * name from the certificate and says separately what it is bound to.
104
+ */
105
+ export declare function securityBanner(security: Security, port: number): string[];
106
+ /** An environment switch. Shared with main.ts so the two cannot parse one variable two ways. */
107
+ export declare function flag(raw: string | undefined, fallback: boolean): boolean;
108
+ export declare function authKeys(token: string): AuthKeys;
109
+ /**
110
+ * Compare a presented token. Hashed first so both sides are 32 bytes: a length
111
+ * mismatch is a plain `false`, never the RangeError `timingSafeEqual` throws.
112
+ */
113
+ export declare function tokenMatches(keys: AuthKeys, presented: string): boolean;
114
+ /**
115
+ * The token in an `Authorization` header, or null when the header is anything
116
+ * but `Bearer <token>` — Basic, an empty Bearer, a second parameter. The scheme
117
+ * is case-insensitive (RFC 9110 §11.1).
118
+ */
119
+ export declare function bearerOf(header: string): string | null;
120
+ /**
121
+ * The cookie's name, WITH THE PORT IN IT.
122
+ *
123
+ * `__Host-` over TLS: the browser then refuses the cookie unless it is `Secure`,
124
+ * `Path=/` and has no `Domain`, so nothing on a sibling host can plant or read
125
+ * it. Plain loopback cannot use the prefix, which requires `Secure`.
126
+ *
127
+ * THE PORT IS IN THE NAME BECAUSE COOKIES HAVE NO PORT. RFC 6265 §1 says so
128
+ * outright: `127.0.0.1:8475` and `127.0.0.1:9000` share one jar. Without the
129
+ * suffix a second DeckSmith on this host overwrites the first one's cookie and
130
+ * the viewer is silently logged out of it — and, since both derive the same key
131
+ * from the same token file, each would have accepted the other's session as its
132
+ * own. Neither happens now.
133
+ *
134
+ * IT IS NOT CONFIDENTIALITY. The browser still SENDS this cookie to every
135
+ * listener on the host, whatever the name; scoping only stops them colliding and
136
+ * stops one server honouring another's session. See `sessionBinding`.
137
+ */
138
+ export declare function cookieName(tls: boolean, port: number): string;
139
+ /**
140
+ * What a session is valid FOR: this bind and this port, inside the MAC.
141
+ *
142
+ * The cookie name keeps two instances' cookies apart in the browser. This keeps
143
+ * them apart in the server: a session minted by the instance on :8475 fails the
144
+ * MAC on :9000 even though both derived `sessionKey` from the same token file,
145
+ * so a second instance cannot be logged into with the first one's cookie.
146
+ */
147
+ export declare function sessionBinding(host: string, port: number): string;
148
+ /**
149
+ * A session is `<exp>.<mac>`, stateless: a restart keeps every browser logged
150
+ * in, which is what "Run it again" after a restart needs. Revocation is
151
+ * rotating the token file, because the key is derived from it — POST /logout
152
+ * clears the browser's copy and nothing else, so a value captured before it
153
+ * stays good until `exp`.
154
+ */
155
+ export declare function mintSession(keys: AuthKeys, nowMs: number, bind: string): string;
156
+ export declare function sessionValid(keys: AuthKeys, value: string, nowMs: number, bind: string): boolean;
157
+ /** Every value of cookie `name`. Several may arrive; the caller accepts any valid one. */
158
+ export declare function cookieValues(header: string | undefined, name: string): string[];
159
+ export declare function sessionCookie(name: string, value: string, tls: boolean): string;
160
+ export declare function clearedCookie(name: string, tls: boolean): string;
@@ -1,4 +1,6 @@
1
1
  import { type Server } from "node:http";
2
+ import { type Server as TlsServer } from "node:https";
3
+ import { type AuthKeys, type TlsMaterial } from "./auth.js";
2
4
  import { type PipelineInput } from "./pipeline.js";
3
5
  import { type JobHandle, type JobResult, Queue } from "./queue.js";
4
6
  export interface ServeOptions {
@@ -24,9 +26,27 @@ export interface ServeOptions {
24
26
  * Same seam, and the same reason, as `Runner` in src/plan/codex.ts.
25
27
  */
26
28
  run?: (job: JobHandle, input: PipelineInput) => Promise<JobResult>;
29
+ /**
30
+ * Require the token. Built from DECKSMITH_TOKEN_FILE by `resolveSecurity` in
31
+ * ./auth.ts; absent, the server behaves as it did before tokens existed.
32
+ */
33
+ auth?: AuthKeys;
34
+ /**
35
+ * Serve https. On a bind that is not loopback the certificate's SANs are the
36
+ * names the server answers to.
37
+ */
38
+ tls?: TlsMaterial;
39
+ /** The clock sessions and the failed-token limiter read. Injected by tests. */
40
+ now?: () => number;
41
+ /**
42
+ * TEST SEAM ONLY. `false` drops the anti-framing headers every non-deck
43
+ * response carries, so the browser test can show what they stop. Nothing in
44
+ * src/ passes it.
45
+ */
46
+ frameHeaders?: boolean;
27
47
  }
28
48
  export declare function createDeckServer(opts: ServeOptions): {
29
- server: Server;
49
+ server: Server | TlsServer;
30
50
  queue: Queue;
31
51
  };
32
52
  /**
@@ -54,4 +74,11 @@ export declare class RateLimiter {
54
74
  private readonly now;
55
75
  constructor(limit: number, windowMs: number, now?: () => number);
56
76
  take(key: string): boolean;
77
+ /**
78
+ * Whether `take` would refuse, without spending anything. The failed-token
79
+ * limiter asks this BEFORE comparing, so an exhausted budget refuses a correct
80
+ * token too and the comparison stops being an oracle; it `take`s only after a
81
+ * comparison fails.
82
+ */
83
+ blocked(key: string): boolean;
57
84
  }
@@ -5,5 +5,11 @@
5
5
  * once per call and a server that hot-reloads a theme picks the change up
6
6
  * without a module cache dance. It is a few hundred microseconds of string
7
7
  * concatenation against a request that will take two minutes to satisfy.
8
+ *
9
+ * `auth` puts a logout form in the header. It is a plain form so it works
10
+ * without script, and it is only there when the server has a token to log out
11
+ * of — http.ts caches one page per mode.
8
12
  */
9
- export declare function uiPage(): string;
13
+ export declare function uiPage(opts?: {
14
+ auth?: boolean;
15
+ }): string;
@@ -1650,8 +1650,29 @@ export declare const segmentSchema: z.ZodObject<{
1650
1650
  text: z.ZodString;
1651
1651
  }, z.core.$strip>>;
1652
1652
  }, z.core.$strip>;
1653
+ /**
1654
+ * The canvas a narration's sentences were split against. Recorded so a refusal
1655
+ * can say where the narration was staged and which `narrate` flags stage it
1656
+ * where the build is; it is NOT what is compared. See `assertNarrationStaging`
1657
+ * in src/narrate/narrate.ts.
1658
+ */
1659
+ export declare const narrationCanvasSchema: z.ZodObject<{
1660
+ format: z.ZodString;
1661
+ width: z.ZodNumber;
1662
+ height: z.ZodNumber;
1663
+ captionReserve: z.ZodNumber;
1664
+ }, z.core.$strip>;
1665
+ export type NarrationCanvas = z.infer<typeof narrationCanvasSchema>;
1653
1666
  export declare const narrationSchema: z.ZodObject<{
1654
1667
  voice: z.ZodString;
1668
+ canvas: z.ZodOptional<z.ZodObject<{
1669
+ format: z.ZodString;
1670
+ width: z.ZodNumber;
1671
+ height: z.ZodNumber;
1672
+ captionReserve: z.ZodNumber;
1673
+ }, z.core.$strip>>;
1674
+ stops: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
1675
+ speakingStops: z.ZodOptional<z.ZodNumber>;
1655
1676
  beats: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
1656
1677
  stop: z.ZodInt;
1657
1678
  text: z.ZodString;
@@ -2355,6 +2376,14 @@ export declare const packSchema: z.ZodObject<{
2355
2376
  }, z.core.$strip>;
2356
2377
  narration: z.ZodOptional<z.ZodObject<{
2357
2378
  voice: z.ZodString;
2379
+ canvas: z.ZodOptional<z.ZodObject<{
2380
+ format: z.ZodString;
2381
+ width: z.ZodNumber;
2382
+ height: z.ZodNumber;
2383
+ captionReserve: z.ZodNumber;
2384
+ }, z.core.$strip>>;
2385
+ stops: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
2386
+ speakingStops: z.ZodOptional<z.ZodNumber>;
2358
2387
  beats: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
2359
2388
  stop: z.ZodInt;
2360
2389
  text: z.ZodString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokerized/decksmith",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Turn a source document into an animated explanation deck.",
5
5
  "type": "module",
6
6
  "license": "MIT",