@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.
- package/README.md +177 -32
- package/dist/cli.js +5060 -4955
- package/dist/deck-runtime.js +2 -2
- package/dist/index.js +5947 -5854
- package/dist/mcp.js +3898 -3806
- package/dist/types/deck/runtime.d.ts +40 -0
- package/dist/types/emit/camera.d.ts +8 -0
- package/dist/types/emit/composition.d.ts +9 -1
- package/dist/types/emit/theme.d.ts +17 -13
- package/dist/types/narrate/narrate.d.ts +73 -3
- package/dist/types/render/timing.d.ts +5 -3
- package/dist/types/server/auth.d.ts +160 -0
- package/dist/types/server/http.d.ts +28 -1
- package/dist/types/server/ui.d.ts +7 -1
- package/dist/types/types.d.ts +29 -0
- package/package.json +1 -1
|
@@ -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
|
|
6
|
+
* The theme a storyboard's scenes are staged with, and the font family it ships.
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
/**
|
|
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
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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(
|
|
13
|
+
export declare function uiPage(opts?: {
|
|
14
|
+
auth?: boolean;
|
|
15
|
+
}): string;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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;
|