@getv.design/embed 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,173 @@
1
+ The vDesign Embed Licence, version 1.0
2
+
3
+ Copyright © 2026 Sagar Saini. All rights not granted below are reserved.
4
+
5
+ This licence lets you use the vDesign Embed free of charge, including in
6
+ commercial products, and pass on copies of it as long as they are unchanged.
7
+ By using, copying or redistributing the Software, You accept this licence.
8
+
9
+
10
+ 1. Definitions
11
+
12
+ "Software" means the vDesign Embed: the JavaScript files, TypeScript
13
+ declaration files and stylesheets in this package, in the form the Licensor
14
+ publishes them, and any copy of them. It does not include the third-party
15
+ components described in clause 6.
16
+
17
+ "Licensor" means Sagar Saini, an individual. You can contact the Licensor at
18
+ sagar@getv.design.
19
+
20
+ "You" means the person or organisation that uses, copies or redistributes the
21
+ Software under this licence.
22
+
23
+
24
+ 2. Grant
25
+
26
+ The Licensor grants You a free, worldwide, non-exclusive, non-transferable,
27
+ royalty-free licence to:
28
+
29
+ (a) use the Software for any purpose, including commercial purposes;
30
+ (b) copy it; and
31
+ (c) redistribute it unmodified, including as part of a website or an
32
+ application that You make, sell or give away.
33
+
34
+ The people who use Your website or application may use the Software as part
35
+ of it. They need nothing further from the Licensor to do so.
36
+
37
+
38
+ 3. Redistribution
39
+
40
+ You may redistribute the Software bundled into Your own product, or serve it,
41
+ as this package and under its own name, from npm or a public npm mirror such
42
+ as a content delivery network, as long as:
43
+
44
+ (a) the copyright and licence notices in the Software, including the comment
45
+ at the start of each JavaScript file, are kept, in the code or in a
46
+ notices file that Your build tool produces from it; and
47
+ (b) when You redistribute the package as a whole, this licence and the file
48
+ THIRD_PARTY_NOTICES.txt go with it.
49
+
50
+ Processing the Software with a bundler, minifier, transpiler or similar build
51
+ tool, including tree-shaking, code-splitting and changing the module format,
52
+ is not modification under this licence, as long as it does not change what the
53
+ Software does. Configuring the Software through its documented options,
54
+ styling it through its documented CSS custom properties, and calling its
55
+ public interface are uses of it, not modifications.
56
+
57
+ Everyone who receives a copy of the Software from You receives this licence
58
+ from the Licensor directly.
59
+
60
+
61
+ 4. Restrictions
62
+
63
+ Except as clauses 2 and 3 allow, You may not:
64
+
65
+ (a) modify the Software, or distribute a modified copy of it or a work based
66
+ on it (a product that includes the unmodified Software is not a work
67
+ based on it);
68
+ (b) decompile, disassemble or otherwise reverse engineer the Software;
69
+ (c) remove or change any copyright, licence or attribution notice in the
70
+ Software; or
71
+ (d) misrepresent where the Software came from, including by presenting it as
72
+ Your own work.
73
+
74
+ Restrictions (a) and (b) apply only to the extent the law allows. Where
75
+ applicable law expressly gives You a right to do something despite a
76
+ restriction like these, this licence does not take that right away. Examples
77
+ are Articles 5 and 6 of EU Directive 2009/24/EC (on the legal protection of
78
+ computer programs) and section 52(1)(ab) and (ac) of the Indian Copyright Act,
79
+ 1957, which allow certain acts needed to study how a program works or to make
80
+ an independently created program work with it.
81
+
82
+ Nothing in this licence stops You from making a product that competes with
83
+ vDesign, including one that reads or writes the .vd format.
84
+
85
+
86
+ 5. Your documents and the open format
87
+
88
+ The Licensor claims no rights in the documents You open, render, present or
89
+ edit with the Software, or in what the Software produces from them.
90
+
91
+ The .vd format specification is published separately under the Creative
92
+ Commons Attribution 4.0 International licence (CC BY 4.0). Anyone may
93
+ implement the format, free of charge and without asking permission. This
94
+ licence covers the Software only. See https://getv.design/docs/licence/.
95
+
96
+
97
+ 6. Third-party components
98
+
99
+ This package also contains code and fonts made by others. They are not part
100
+ of the Software, and are listed in THIRD_PARTY_NOTICES.txt with their licences
101
+ in full. Those components are governed by their own licences, not by this one,
102
+ and nothing in this licence limits the rights those licences give You.
103
+
104
+
105
+ 7. Trademarks
106
+
107
+ This licence gives You no rights to the name "vDesign" or to any vDesign logo,
108
+ except to say accurately that Your product uses the Software.
109
+
110
+ You need not show any credit for the Software to the people who use Your
111
+ product: the notices in the code, under clause 3, are the only ones You must
112
+ keep. A visible "Made with vDesign" link is appreciated. The Software shows
113
+ no watermark and needs no licence key.
114
+
115
+
116
+ 8. Network and data
117
+
118
+ The Software sends no telemetry and collects no information about You or the
119
+ people who use Your product. It makes network requests only for:
120
+
121
+ (a) font files, from the configured font location, which is the jsDelivr
122
+ content delivery network (cdn.jsdelivr.net) unless You set another;
123
+ (b) addresses that You, or the documents You give the Software, supply; and
124
+ (c) the Software's own files, from wherever You serve them.
125
+
126
+ These requests are made by the browser or other runtime that runs the
127
+ Software. The services they reach have their own terms and privacy practices.
128
+
129
+
130
+ 9. Versions and termination
131
+
132
+ The Licensor may publish later versions of the Software under different terms.
133
+ That does not change the licence of a version already published: the licence a
134
+ version was published with is not revoked, except under this clause.
135
+
136
+ If You breach this licence and do not cure the breach within 30 days after the
137
+ Licensor notifies You of it, Your rights under this licence end, and You must
138
+ stop using and redistributing the Software. That does not end the licence of
139
+ anyone who received the Software from You and complies with this licence.
140
+
141
+
142
+ 10. No warranty; limitation of liability
143
+
144
+ The Software is provided free of charge, "as is" and "as available", without
145
+ warranty of any kind, express or implied, including any warranty of
146
+ merchantability, fitness for a particular purpose, title or non-infringement,
147
+ to the fullest extent the law allows. You use it at Your own risk.
148
+
149
+ To the fullest extent the law allows, the Licensor is not liable for any
150
+ indirect, incidental, special, consequential, exemplary or punitive damages,
151
+ or for any loss of profits, revenue, data, business or goodwill, arising out of
152
+ or in connection with the Software or this licence, however caused and on any
153
+ theory of liability, even if the Licensor was told such loss was possible.
154
+
155
+ To the fullest extent the law allows, the Licensor's total liability arising
156
+ out of or in connection with the Software or this licence is limited to the
157
+ greater of the amount You paid the Licensor for the Software and 100 US
158
+ dollars (USD 100).
159
+
160
+ Nothing in this licence excludes or limits any liability that cannot be
161
+ excluded or limited by law, such as liability for fraud, or for death or
162
+ personal injury caused by negligence.
163
+
164
+
165
+ 11. Governing law and courts
166
+
167
+ This licence, and any dispute arising out of or in connection with it or the
168
+ Software, are governed by the laws of India. The courts in India that have
169
+ jurisdiction over the dispute have exclusive jurisdiction to settle it.
170
+
171
+ This clause does not take away any protection You have, as a consumer, under
172
+ the mandatory law of the country where You live, or any right You have under
173
+ that law to bring proceedings in its courts.
package/README.md CHANGED
@@ -1,3 +1,423 @@
1
- # Temporary Holding Version
1
+ # @getv.design/embed
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Render, present and edit vDesign (`.vd`) documents in any web page.
4
+
5
+ Free to use, including commercially: no licence key, no watermark, and
6
+ attribution is appreciated but not required. The code is proprietary, under
7
+ the vDesign Embed Licence in `LICENSE`; the `.vd` format it reads is open, and
8
+ anyone may implement it (see "Licence" below).
9
+
10
+ **Not published yet.** This package is being built in steps; until the first
11
+ release, nothing here is on npm, and the API may still change. While it is
12
+ 0.x, a minor version may change the API.
13
+
14
+ The API reference, every option and event, is at
15
+ https://getv.design/docs/embed/api/.
16
+
17
+ ## Quick start
18
+
19
+ From npm, with a bundler:
20
+
21
+ ```sh
22
+ npm install @getv.design/embed
23
+ ```
24
+
25
+ ```js
26
+ import { view } from '@getv.design/embed/view';
27
+
28
+ const text = '…'; // a .vd document, as text; or pass a URL or a Blob
29
+ const handle = await view(document.querySelector('#design'), text);
30
+ ```
31
+
32
+ Without a bundler, import the ES modules from a CDN. jsDelivr serves the
33
+ package as published; import the entry file itself (not jsDelivr's `+esm`
34
+ rebundling), so the editor and the font code still load only when needed:
35
+
36
+ ```html
37
+ <div id="design"></div>
38
+ <script type="module">
39
+ import { view } from 'https://cdn.jsdelivr.net/npm/@getv.design/embed@0.1.1/dist/view.js';
40
+
41
+ const text = await (await fetch('/designs/home.vd')).text();
42
+ await view(document.querySelector('#design'), text);
43
+ </script>
44
+ ```
45
+
46
+ Give the element a size in your stylesheet: the view fills it. Under a
47
+ content security policy that refuses inline scripts, put the module in a file
48
+ of your own and link the stylesheets as "Styles and content security policy"
49
+ below describes.
50
+
51
+ The entries:
52
+
53
+ | Import | What it does |
54
+ | ---------------------------------- | ------------------------------------------------------------------ |
55
+ | `@getv.design/embed/static` | A document in, an SVG string out. No DOM: Node 22.3+ and browsers. |
56
+ | `@getv.design/embed/view` | Show a document in an element, page through it, present it. |
57
+ | `@getv.design/embed/edit` | The vDesign editor, with its floating toolbar. |
58
+ | `@getv.design/embed` | `mount(el, source, { mode })`: loads one of the above on demand. |
59
+ | `@getv.design/embed/view.css`, `@getv.design/embed/edit.css` | The styles, for a strict content security policy. |
60
+ | `@getv.design/embed/fonts/*` | The Inter faces, to serve yourself. |
61
+
62
+ ## Static SVG
63
+
64
+ `@getv.design/embed/static`: a document in, an SVG string out, with no DOM. Runs in
65
+ Node 22.3+ and in browsers. 22.3 is when `process.getBuiltinModule`
66
+ arrived, which is how exact fonts are read from disk without a Node import.
67
+
68
+ ```js
69
+ import { parse, renderSvg } from '@getv.design/embed/static';
70
+
71
+ const { document, problems } = parse(text); // never throws; problems lists what is wrong
72
+ const svg = await renderSvg(document); // or renderSvg(text), a URL, or a Blob
73
+ const second = await renderSvg(document, { page: 1 }); // one page, by 0-based index or id
74
+ ```
75
+
76
+ By default text is measured approximately, without font files, so line breaks
77
+ can differ slightly from the app's. `fonts: 'exact'` lays text out from the
78
+ font files the app exports with, so lines break where the app's SVG export
79
+ breaks them:
80
+
81
+ ```js
82
+ const svg = await renderSvg(text, {
83
+ fonts: 'exact',
84
+ onProblems: (problems) => console.log(problems),
85
+ });
86
+ ```
87
+
88
+ In Node the faces are read from the package's own `fonts/` folder; in a
89
+ browser they are fetched from `fontBase` (see "Fonts" below). The disk is used
90
+ only where Node can actually read it: a browser bundle whose bundler kept a
91
+ `file:` URL from the build machine (webpack 5 does) fetches from `fontBase`
92
+ like any other page. The SVG names
93
+ the family, `Inter`, and carries no font files of its own. If a face cannot be
94
+ read, the SVG is drawn approximately and `onProblems` is given a
95
+ `fonts-unavailable` warning; `renderSvg` does not reject for it.
96
+
97
+ ## View and present
98
+
99
+ `@getv.design/embed/view`: show a document in an element, page through it, and present it.
100
+
101
+ ```js
102
+ import { view } from '@getv.design/embed/view';
103
+
104
+ const handle = await view(el, text, { fit: 'page', page: 0 }); // or a URL, a Blob, a parsed document
105
+ handle.on('problems', (problems) => console.log(problems)); // called at once with the current list
106
+ handle.goTo('pricing'); // a page by id or 0-based index
107
+ await handle.load(newText); // redraws in place; unchanged parts are not rebuilt
108
+ const play = await handle.play(); // present over the view; Escape comes back
109
+ handle.destroy(); // el is left empty, and image URLs are released
110
+ ```
111
+
112
+ The view adds one `<div>` inside `el` and fills it, so give `el` a size. A
113
+ plain wheel over it scrolls your page as usual; Ctrl or ⌘ with the wheel, or a
114
+ trackpad pinch, zooms, and middle-drag or Space-drag pans. `interactive: false`
115
+ turns all of that off. A page
116
+ is a top-level frame that is not hidden, counted the same way by `renderSvg`.
117
+ Images are drawn from `blob:` URLs, so a page with a content security policy
118
+ needs `img-src blob:`.
119
+
120
+ `mount(el, source, { mode: 'view' })` from `@getv.design/embed` does the same and loads the
121
+ view code only when called; `mode: 'play'` opens straight into the
122
+ presentation, and that handle's `destroy()` removes the view under it too.
123
+
124
+ The view inserts one `<style>` into the page's head; "Styles and content
125
+ security policy" below has the alternative for a page that refuses inline
126
+ styles.
127
+
128
+ ## Edit
129
+
130
+ `@getv.design/embed/edit`: the vDesign editor in your page, with its floating toolbar.
131
+
132
+ ```js
133
+ import { edit } from '@getv.design/embed/edit';
134
+
135
+ const editor = await edit(el, text, { fit: 'page' }); // the same sources and options as view()
136
+ editor.on('change', (event) => save(event.text())); // once per undo step; text() is the whole .vd file
137
+ editor.getText(); // the same, at any time: the design, then any images
138
+ editor.undo();
139
+ editor.redo();
140
+ const handle = await editor.view(); // back to a view, where the person was looking
141
+ editor.destroy(); // el is left empty
142
+ ```
143
+
144
+ From a view, `await handle.edit()` swaps the view for an editor in the same
145
+ element and keeps the camera; `mount(el, source, { mode: 'edit' })` opens
146
+ straight into one. The editor's code (about 85 KB gzip) loads only then:
147
+ nothing that views a document downloads it.
148
+
149
+ `change` fires once for each step undo would take back: a whole drag is one,
150
+ when the pointer comes up, and so are an undo and a redo. `text()` composes the
151
+ file only when you call it. A picture pasted or dropped onto the canvas goes
152
+ into the document's own payload, so `getText()` carries it and nothing is
153
+ uploaded anywhere. `toolbar: false` leaves out the toolbar and the context
154
+ menu, for a host with its own controls.
155
+
156
+ The editor behaves on a page as the view does: a plain wheel scrolls your
157
+ page, Ctrl or ⌘ with the wheel zooms, and any number of editors can share a
158
+ page. A plain click selects, so a link is followed on ⌘-click (Ctrl elsewhere),
159
+ and only an `https:` or `mailto:` one: to `onLink`, or else into a new tab
160
+ with `noopener`. `fit` frames the document when the editor opens, and again
161
+ when `el` changes size only until the person moves the camera.
162
+
163
+ The editor's chrome is drawn for the default dark `surface`. With a light
164
+ one, also set `--vd-page-label` and `--vd-page-label-hover` on `el` (the
165
+ colours of each page's name above its corner), for example to
166
+ `rgb(0 0 0 / 0.5)` and `#1a1a1f`.
167
+
168
+ ## Fonts
169
+
170
+ By default (`fonts: 'exact'`) the view lays text out from the same font files
171
+ the app exports with, so lines break where the app breaks them. It draws at
172
+ once with your page's fonts, loads the font code (about 150 KB gzip, only now),
173
+ fetches only the faces the document uses (one `.woff2` per weight and style),
174
+ and lays the text out again once when they arrive. A document opened later
175
+ with `load()` has any new face fetched before it is drawn.
176
+ `handle.on('fonts', (state) => …)` says `'exact'` once the files are in use.
177
+
178
+ If a face cannot be fetched, the view stays on your page's fonts, `problems`
179
+ gains a `fonts-unavailable` warning and the `fonts` event says `'browser'`;
180
+ nothing throws. `fonts: 'browser'` skips all of this.
181
+
182
+ The editor measures the same way. A face that an edit brings in, such as a
183
+ word made bold, is fetched once the document has been still for a moment. If
184
+ one cannot be fetched the editor carries on with your page's fonts; the editor
185
+ has no `problems` or `fonts` event yet to say so.
186
+
187
+ ### Where the faces come from
188
+
189
+ The faces come from `fontBase`, by default this package's `fonts/` folder on
190
+ jsDelivr (`https://cdn.jsdelivr.net/npm/@getv.design/embed@0.1.1/fonts/`). That means
191
+ your readers' browsers request the faces from jsDelivr, and a content security
192
+ policy needs it in `connect-src`. To keep those requests on your own origin,
193
+ copy `node_modules/@getv.design/embed/fonts/` to your site and point `fontBase` at it:
194
+
195
+ ```js
196
+ await view(el, text, { fontBase: '/assets/vdesign-fonts/' });
197
+ ```
198
+
199
+ The faces are Inter, under the SIL Open Font License (`fonts/OFL.txt`).
200
+
201
+ ## Styles and content security policy
202
+
203
+ The embed's styles reach the page by one of two routes.
204
+
205
+ 1. **Injected (the default).** The view inserts one
206
+ `<style id="vd-viewer-styles">` into the page's head, and the editor
207
+ another, `<style id="vd-editor-styles">`. Nothing to set up, but a
208
+ content security policy must allow `style-src 'unsafe-inline'`.
209
+ 2. **Linked.** Serve the package's `view.css` (and `edit.css`, if you edit)
210
+ and link them under those ids. The view and the editor find the id and
211
+ insert nothing, so the page needs no `'unsafe-inline'`:
212
+
213
+ ```html
214
+ <link rel="stylesheet" id="vd-viewer-styles" href="/view.css" />
215
+ <link rel="stylesheet" id="vd-editor-styles" href="/edit.css" />
216
+ ```
217
+
218
+ Link them before the embed runs, from your own origin or from the CDN
219
+ (`https://cdn.jsdelivr.net/npm/@getv.design/embed@0.1.1/view.css`). The id is what
220
+ the embed looks for: linked without it, the stylesheet still works, but
221
+ the embed inserts its `<style>` as well.
222
+
223
+ Either way, the theme is a set of CSS custom properties, which you can set on
224
+ the element you give the embed; `--vd-accent`, for example, colours the
225
+ selection.
226
+
227
+ The view, the player and the editor run under a policy with no
228
+ `'unsafe-inline'` once the styles are linked. `npm run embed:check` runs
229
+ every mode under this one, with the package served from the page's own
230
+ origin:
231
+
232
+ ```text
233
+ default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'
234
+ ```
235
+
236
+ What each part is for:
237
+
238
+ - `script-src`: wherever you serve the package's `dist/` from.
239
+ - `style-src`: wherever you serve `view.css` and `edit.css` from. Without them
240
+ linked, the embed injects a `<style>` and needs `'unsafe-inline'`.
241
+ - `img-src blob:` for a document's images, which are drawn from object URLs.
242
+ `img-src data:` for the editor, whose cursors are `data:` images.
243
+ - `connect-src`: the document URLs you pass, and `fontBase`. With the default
244
+ `fontBase`, that means `https://cdn.jsdelivr.net`; served yourself, your own
245
+ origin.
246
+ - `font-src` needs nothing for the embed: the faces are fetched as bytes and
247
+ handed to the page as `FontFace`s, which no `font-src` governs.
248
+
249
+ Served from jsDelivr instead, the same policy needs
250
+ `https://cdn.jsdelivr.net` in `script-src`, `style-src` and `connect-src`.
251
+
252
+ `renderSvg` output carries a `style="mix-blend-mode: …"` attribute on any
253
+ layer with a blend mode. Inlined into a page under `style-src 'self'`, those
254
+ layers lose their blend mode and draw normally; shown as an `<img>`, the SVG
255
+ is unaffected.
256
+
257
+ ## Host page CSS
258
+
259
+ There is no shadow root, so your page's CSS reaches the embed. Every class it
260
+ uses starts with `vd-`, which keeps its rules and yours from colliding, but a
261
+ reset that targets elements still applies. `npm run embed:check` runs the
262
+ editor under a reset like a CSS framework's preflight (`svg { display: block }`,
263
+ `button { all: unset }`, `* { color: red }`, border-box sizing), and this is
264
+ what it does:
265
+
266
+ - **The drawing is unaffected.** A document's colours, sizes and fonts are
267
+ attributes on its SVG, which element selectors do not reach, and the theme
268
+ variables still apply: `--vd-accent` set on your element colours the
269
+ selection.
270
+ - **`* { color: … }` recolours the editor's chrome.** The toolbar's icons and
271
+ the context menu's labels take your colour, and a disabled menu item no
272
+ longer looks dimmed, because each element gets the colour directly rather
273
+ than inheriting the editor's.
274
+ - **`button { all: unset }` and border-box resets resize the chrome.** The
275
+ toolbar grows (348 by 32 pixels became 376 by 44), and the context menu's
276
+ shortcut column is cut off at its right edge. The view draws no buttons, so
277
+ only the editor is affected.
278
+
279
+ If your stylesheet has rules like these, scope them to your own content so
280
+ they do not match inside the element you give the embed.
281
+
282
+ ## Not yet
283
+
284
+ - **Arrow keys do not move a selected shape** in the editor. They move a
285
+ path's points while you edit them, and nothing else yet; drag instead.
286
+ - **No `problems` or `fonts` event on the editor**, so a face it could not
287
+ fetch goes unreported (see "Fonts").
288
+
289
+ Not planned for the first release, in no particular order: shadow-root
290
+ isolation, the inspector and layers panels, a custom element, React or Vue
291
+ wrappers, an IIFE or UMD build, images in Node.
292
+
293
+ ## Attribution
294
+
295
+ Not required. The notices in the code, the comment at the top of each file,
296
+ are the only credit the licence asks you to keep, and a bundler that moves
297
+ them into a notices file keeps them well enough. If you would like to say
298
+ where your designs come from, a "Made with vDesign" link to
299
+ https://getv.design is appreciated.
300
+
301
+ ## Licence
302
+
303
+ Free to use under the vDesign Embed Licence, in `LICENSE`: commercial use and
304
+ unmodified redistribution are allowed, attribution is appreciated but not
305
+ required, and there is no watermark or licence key. Bundling, minifying and
306
+ code-splitting it are not modification. The code is proprietary: you may not
307
+ change it or pass on a changed copy. The `.vd` format itself is open, under
308
+ CC BY 4.0, and anyone may implement it: see https://getv.design/docs/licence/. Third-party code and fonts in the
309
+ package, and their licences in full, are listed in `THIRD_PARTY_NOTICES.txt`.
310
+
311
+ ## Working on it
312
+
313
+ The name above is read from `publishName` in this workspace's `package.json`,
314
+ the only place it is written. `npm run embed:build` writes the package into
315
+ `out/`; `npm run embed:pack` builds it, packs it into a tarball in
316
+ `packages/embed/` (gitignored, never published), and prints the tarball's
317
+ contents.
318
+ `out/package.json` stays `"private": true` until the licence has been reviewed
319
+ (below).
320
+
321
+ `npm run embed:check` builds the package and runs it in Chromium
322
+ (Playwright, from `check/`): host pages under a strict content security policy
323
+ with no `'unsafe-inline'`, the styles linked as above and the faces served
324
+ from the same origin. It covers `/static`, `/view`, presenting and `/edit`,
325
+ exact fonts and their failure, the wheel, links, several embeds on one page,
326
+ `destroy()`, and the reset-heavy host above. It needs a browser
327
+ (`npx playwright install chromium`), so it is not part of `npm run verify`;
328
+ run it when you change this package or anything it bundles.
329
+
330
+ The build writes `THIRD_PARTY_NOTICES.txt` from esbuild's metafile
331
+ (`scripts/notices.mjs`), so it lists exactly the packages whose code ships. It
332
+ fails on a licence outside MIT, ISC, Apache-2.0, 0BSD, BSD-2-Clause,
333
+ BSD-3-Clause and OFL-1.1, and on a package with no licence file unless
334
+ `notices-overrides.json` says where its text came from (an override may also
335
+ fill a missing licence, never replace one). Code that is neither under
336
+ `node_modules` nor in one of our workspaces, such as a linked package, is
337
+ refused, and a refused build leaves no `out/`. Adding a licence to
338
+ that list is a licensing decision, not a build fix. Every published `.js` file
339
+ starts with a licence banner, and licence comments in bundled code are kept at
340
+ the end of the file.
341
+
342
+ ## Publishing (owner only)
343
+
344
+ **This package is published only by `.github/workflows/publish-embed.yml`.
345
+ Nobody, person or agent, runs `npm publish` locally**; `--dry-run` is the one
346
+ exception.
347
+
348
+ On every merge to `main` that touches this package, the workspaces it bundles
349
+ (core, engine, renderer, fonts, viewer, editor), `package-lock.json` or the
350
+ workflow, the workflow installs, runs this package's tests and
351
+ `npm run embed:check`, builds `out/`, runs the publish gate, and then asks
352
+ `scripts/publish-decision.mjs`:
353
+
354
+ - **Publish:** the version in `package.json` is not on npm. The workflow
355
+ publishes the `out/` it just checked, with `--access public`, then waits up
356
+ to five minutes for jsDelivr to serve a font from it.
357
+ - **Skip:** that version is on npm, and the published tarball holds exactly
358
+ the files this build packs. Nothing that ships changed.
359
+ - **Bump needed:** that version is on npm with different files. The run
360
+ fails with "bump the version in packages/embed/package.json".
361
+
362
+ A pull request runs the same check, with no token in reach, and its summary
363
+ says which of the three its merge would do; a bump shows red there first. So a
364
+ PR that changes what ships, this README included, raises `version` in the same
365
+ PR. Locally, after `npm run embed:build`, run
366
+ `node packages/embed/scripts/publish-decision.mjs` to ask the same question.
367
+
368
+ The comparison works because the build is deterministic: two clean builds give
369
+ byte-identical `out/` and tarballs. Keep it so. A timestamp, an absolute path
370
+ or an unordered list in the output would make every merge look like a change.
371
+
372
+ The workflow authenticates with the `NPM_TOKEN` secret: a granular npm token
373
+ with read and write on the package's scope and nothing else, allowed to bypass
374
+ 2FA. It expires; renew it before it does, or the next publish fails at the
375
+ publish step. There is no provenance, which npm makes only from a public
376
+ repository. Once the package exists, npm trusted publishing (OIDC) can replace
377
+ the token: register the workflow on npmjs.com as the package's trusted
378
+ publisher (it already has `id-token: write`), then delete the secret. Whether
379
+ npm accepts trusted publishing from a private repository is unconfirmed, so
380
+ try it with the token still in place.
381
+
382
+ ### The licence gate
383
+
384
+ Nothing publishes until the licence has been reviewed by a lawyer and the owner
385
+ has committed a record of that review. `scripts/gate.mjs` is the lock: the build
386
+ writes `"private": false` into `out/package.json` only when it passes, the
387
+ workflow runs it before deciding, and the manifest's `prepublishOnly` runs it
388
+ again, so `npm publish` (and `npm publish --dry-run`) from `out/` fails while it
389
+ is closed. Notes for the reviewer are in `LICENSE-REVIEW.md`, which is not
390
+ packed.
391
+
392
+ **Agents never create or edit `legal-review.json`.** The record is the owner's
393
+ statement that the review happened. To make it:
394
+
395
+ 1. Have `LICENSE` reviewed, with `LICENSE-REVIEW.md` as the brief. Make the
396
+ changes advised, and remove the `DRAFT` line at the top.
397
+ 2. Hash the licence exactly as it will ship:
398
+
399
+ ```sh
400
+ shasum -a 256 packages/embed/LICENSE
401
+ ```
402
+
403
+ 3. Commit `packages/embed/legal-review.json`, with the hash from step 2:
404
+
405
+ ```json
406
+ {
407
+ "reviewed": true,
408
+ "by": "Name of the reviewer",
409
+ "date": "2026-11-02",
410
+ "licenceSha256": "the 64 hex digits shasum printed"
411
+ }
412
+ ```
413
+
414
+ The workflow never publishes from a saved `.tgz` and never with
415
+ `--ignore-scripts`: npm runs `prepublishOnly` only when publishing a folder, so
416
+ either would skip the gate's second check and rely on `"private"` alone.
417
+ `.gitattributes` marks `packages/embed/LICENSE` as `-text`, so git never
418
+ rewrites its line endings and the hash is the same on every platform.
419
+
420
+ Any later edit to `LICENSE` closes the gate again until the record's hash is
421
+ updated, so the text that ships is always the text that was reviewed. Run
422
+ `node packages/embed/scripts/gate.mjs` to see whether the gate is open and why
423
+ not.