handoff-app 2.0.0-2 → 2.0.0-3

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 (161) hide show
  1. package/README.md +140 -83
  2. package/dist/app-builder/build.js +21 -4
  3. package/dist/app-builder/client-config.js +9 -8
  4. package/dist/cli/auth/browser.d.ts +2 -0
  5. package/dist/cli/auth/browser.js +33 -0
  6. package/dist/cli/auth/device.d.ts +11 -0
  7. package/dist/cli/auth/device.js +155 -0
  8. package/dist/cli/auth/store.d.ts +29 -0
  9. package/dist/cli/auth/store.js +146 -0
  10. package/dist/cli/scaffold.js +20 -154
  11. package/dist/commands/index.js +4 -0
  12. package/dist/commands/login/index.d.ts +8 -0
  13. package/dist/commands/login/index.js +53 -0
  14. package/dist/commands/logout/index.d.ts +7 -0
  15. package/dist/commands/logout/index.js +54 -0
  16. package/dist/config/entries.d.ts +47 -0
  17. package/dist/config/entries.js +162 -0
  18. package/dist/config/index.d.ts +2 -0
  19. package/dist/config/index.js +5 -1
  20. package/dist/config/runtime.d.ts +9 -9
  21. package/dist/config/runtime.js +15 -15
  22. package/dist/registry/auth/access-tokens.d.ts +46 -0
  23. package/dist/registry/auth/access-tokens.js +165 -0
  24. package/dist/registry/auth/action-tokens.d.ts +66 -0
  25. package/dist/registry/auth/action-tokens.js +178 -0
  26. package/dist/registry/auth/crypto.d.ts +4 -0
  27. package/dist/registry/auth/crypto.js +16 -0
  28. package/dist/registry/auth/database.d.ts +7 -0
  29. package/dist/registry/auth/database.js +22 -0
  30. package/dist/registry/auth/device-authorizations.d.ts +51 -0
  31. package/dist/registry/auth/device-authorizations.js +170 -0
  32. package/dist/registry/auth/gravatar.d.ts +1 -0
  33. package/dist/registry/auth/gravatar.js +8 -0
  34. package/dist/registry/auth/index.d.ts +11 -0
  35. package/dist/registry/auth/index.js +72 -0
  36. package/dist/registry/auth/installation.d.ts +42 -0
  37. package/dist/registry/auth/installation.js +105 -0
  38. package/dist/registry/auth/password.d.ts +12 -0
  39. package/dist/registry/auth/password.js +58 -0
  40. package/dist/registry/auth/rate-limits.d.ts +22 -0
  41. package/dist/registry/auth/rate-limits.js +75 -0
  42. package/dist/registry/auth/types.d.ts +26 -0
  43. package/dist/registry/auth/types.js +9 -0
  44. package/dist/registry/auth/user-record.d.ts +13 -0
  45. package/dist/registry/auth/user-record.js +16 -0
  46. package/dist/registry/auth/users.d.ts +31 -0
  47. package/dist/registry/auth/users.js +135 -0
  48. package/dist/registry/auth/validation.d.ts +3 -0
  49. package/dist/registry/auth/validation.js +28 -0
  50. package/dist/registry/checkout/assets.js +4 -2
  51. package/dist/registry/checkout/index.d.ts +1 -1
  52. package/dist/registry/checkout/index.js +101 -23
  53. package/dist/registry/checkout/tokens.js +4 -2
  54. package/dist/registry/connection.d.ts +9 -0
  55. package/dist/registry/connection.js +29 -1
  56. package/dist/registry/db/driver.d.ts +5 -4
  57. package/dist/registry/db/driver.js +5 -4
  58. package/dist/registry/db/index.d.ts +1 -1
  59. package/dist/registry/db/index.js +7 -1
  60. package/dist/registry/db/schema.d.ts +2521 -750
  61. package/dist/registry/db/schema.js +126 -1
  62. package/dist/registry/publish/assets.js +1 -1
  63. package/dist/registry/publish/errors.js +3 -3
  64. package/dist/registry/publish/index.d.ts +1 -1
  65. package/dist/registry/publish/index.js +9 -9
  66. package/dist/registry/publish/tokens.js +1 -1
  67. package/dist/types/config.d.ts +4 -1
  68. package/docs/cli.md +4 -0
  69. package/drizzle/0005_add_registry_auth.sql +95 -0
  70. package/drizzle/meta/0005_snapshot.json +1926 -0
  71. package/drizzle/meta/_journal.json +8 -1
  72. package/package.json +5 -4
  73. package/src/app/components/Auth/AccountLayout.tsx +87 -0
  74. package/src/app/components/Auth/AuthControls.tsx +99 -0
  75. package/src/app/components/Auth/AuthShell.tsx +73 -0
  76. package/src/app/components/Auth/RegistryInstallGate.tsx +23 -0
  77. package/src/app/components/Auth/RegistrySessionProvider.tsx +15 -0
  78. package/src/app/components/Auth/api.ts +20 -0
  79. package/src/app/components/Layout/Header.tsx +2 -0
  80. package/src/app/components/Navigation/MobileNav.tsx +2 -0
  81. package/src/app/lib/auth/api.ts +113 -0
  82. package/src/app/lib/auth/config.ts +175 -0
  83. package/src/app/lib/auth/email.ts +34 -0
  84. package/src/app/lib/docs-api/runtime-config.ts +0 -14
  85. package/src/app/lib/registry-api/auth.ts +29 -46
  86. package/src/app/lib/registry-api/errors.ts +2 -2
  87. package/src/app/lib/registry-api/handler.ts +19 -14
  88. package/src/app/lib/registry-api/token-transfer.ts +1 -1
  89. package/src/app/lib/registry-api/transfer.ts +4 -4
  90. package/src/app/next.config.mjs +12 -3
  91. package/src/app/pages/_app.tsx +10 -4
  92. package/src/app/pages/account/index.tsx +153 -0
  93. package/src/app/pages/account/tokens.tsx +230 -0
  94. package/src/app/pages/account/users.tsx +310 -0
  95. package/src/app/pages/api/account/index.ts +26 -0
  96. package/src/app/pages/api/account/tokens/[id].ts +17 -0
  97. package/src/app/pages/api/account/tokens/index.ts +44 -0
  98. package/src/app/pages/api/admin/users/[id]/[action].ts +89 -0
  99. package/src/app/pages/api/admin/users/index.ts +53 -0
  100. package/src/app/pages/api/auth/[...nextauth].ts +4 -0
  101. package/src/app/pages/api/auth/request-reset.ts +53 -0
  102. package/src/app/pages/api/auth/reset-password.ts +29 -0
  103. package/src/app/pages/api/install.ts +102 -0
  104. package/src/app/pages/api/oauth/device/approve.ts +23 -0
  105. package/src/app/pages/api/oauth/device/index.ts +41 -0
  106. package/src/app/pages/api/oauth/revoke.ts +24 -0
  107. package/src/app/pages/api/oauth/token.ts +31 -0
  108. package/src/app/pages/api/registry/health.ts +4 -3
  109. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +3 -3
  110. package/src/app/pages/api/registry/transfer/assets/[collection].ts +2 -1
  111. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +2 -1
  112. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +2 -2
  113. package/src/app/pages/cli/device.tsx +143 -0
  114. package/src/app/pages/install.tsx +336 -0
  115. package/src/app/pages/login.tsx +117 -0
  116. package/src/app/pages/reset-password.tsx +159 -0
  117. package/src/app/types/next-auth.d.ts +27 -0
  118. package/src/app-builder/build.ts +22 -5
  119. package/src/app-builder/client-config.ts +10 -9
  120. package/src/cli/auth/browser.ts +29 -0
  121. package/src/cli/auth/device.ts +181 -0
  122. package/src/cli/auth/store.ts +137 -0
  123. package/src/cli/scaffold.ts +20 -197
  124. package/src/commands/index.ts +4 -0
  125. package/src/commands/login/index.ts +48 -0
  126. package/src/commands/logout/index.ts +51 -0
  127. package/src/config/entries.ts +175 -0
  128. package/src/config/index.ts +4 -0
  129. package/src/config/runtime.ts +15 -14
  130. package/src/registry/auth/access-tokens.ts +198 -0
  131. package/src/registry/auth/action-tokens.ts +225 -0
  132. package/src/registry/auth/crypto.ts +13 -0
  133. package/src/registry/auth/database.ts +15 -0
  134. package/src/registry/auth/device-authorizations.ts +223 -0
  135. package/src/registry/auth/gravatar.ts +6 -0
  136. package/src/registry/auth/index.ts +89 -0
  137. package/src/registry/auth/installation.ts +117 -0
  138. package/src/registry/auth/password.ts +46 -0
  139. package/src/registry/auth/rate-limits.ts +84 -0
  140. package/src/registry/auth/types.ts +34 -0
  141. package/src/registry/auth/user-record.ts +25 -0
  142. package/src/registry/auth/users.ts +145 -0
  143. package/src/registry/auth/validation.ts +24 -0
  144. package/src/registry/checkout/assets.ts +4 -2
  145. package/src/registry/checkout/index.ts +114 -24
  146. package/src/registry/checkout/tokens.ts +4 -2
  147. package/src/registry/connection.ts +23 -0
  148. package/src/registry/db/driver.ts +5 -4
  149. package/src/registry/db/index.ts +12 -0
  150. package/src/registry/db/schema.ts +174 -1
  151. package/src/registry/publish/assets.ts +1 -1
  152. package/src/registry/publish/errors.ts +3 -3
  153. package/src/registry/publish/index.ts +9 -9
  154. package/src/registry/publish/tokens.ts +1 -1
  155. package/src/types/config.ts +4 -1
  156. package/templates/default/README.md.tpl +18 -0
  157. package/templates/default/gitignore.tpl +1 -0
  158. package/templates/default/handoff.config.js.tpl +0 -1
  159. package/templates/default/handoff.config.ts.tpl +0 -1
  160. package/templates/default/handoff.config.with-examples.js.tpl +0 -1
  161. package/templates/default/handoff.config.with-examples.ts.tpl +0 -1
package/README.md CHANGED
@@ -19,7 +19,7 @@ authoring locally or running a shared, deployed catalog.
19
19
  * [Requirements](#requirements)
20
20
  * [Quick start](#quick-start)
21
21
  * [Project layout](#project-layout)
22
- * [Authoring components and patterns](#authoring-components-and-patterns)
22
+ * [Authoring components, patterns, and pages](#authoring-components-patterns-and-pages)
23
23
  * [Fetching design tokens](#fetching-design-tokens)
24
24
  * [Running locally](#running-locally)
25
25
  * [Building artifacts](#building-artifacts)
@@ -30,9 +30,9 @@ authoring locally or running a shared, deployed catalog.
30
30
  * [Configuration](#configuration)
31
31
  * [The runtime block](#the-runtime-block)
32
32
  * [Environment variables](#environment-variables)
33
+ * [Registry installation and access](#registry-installation-and-access)
33
34
  * [Hooks](#hooks)
34
35
  * [CLI reference](#cli-reference)
35
- * [Further reading](#further-reading)
36
36
  * [Maintainers](#maintainers)
37
37
  * [Contributing](#contributing)
38
38
  * [License](#license)
@@ -48,16 +48,16 @@ It brings a few pieces together:
48
48
  * **Token extraction.** Pull standardized foundations from your design source and
49
49
  store them as JSON. Figma is supported out of the box through the Figma REST
50
50
  API.
51
- * **Transformation pipeline.** Produce SCSS, CSS, Style Dictionary, and preview
52
- snippets from that data.
51
+ * **Transformation pipeline.** Produce SCSS and CSS by default, with optional
52
+ Style Dictionary or custom transformer output.
53
53
  * **Documentation runtime.** Render live, working previews of your components,
54
54
  tokens, and styles, served through a stable docs read API.
55
55
  * **Delivery tooling.** Build and ship the deliverables, either as a static site
56
56
  or as a deployed, database-backed catalog.
57
57
 
58
- Out of the box Handoff ships SCSS and CSS maps for [Bootstrap 5](https://getbootstrap.com/).
59
- If you use another framework or custom CSS, you can write your own map files to
60
- connect the generated tokens to your project.
58
+ Use the global `entries.scss` and `entries.js` files to load a framework or your
59
+ own frontend code into previews. For example, a Bootstrap project can import
60
+ [Bootstrap 5](https://getbootstrap.com/) from its Sass and JavaScript entries.
61
61
 
62
62
  You install `handoff-app` as a project-local dependency and run its commands in
63
63
  your project's context, so every project pins its own toolchain version.
@@ -75,13 +75,12 @@ badge so the active mode is always clear.
75
75
  server, and build a static documentation snapshot. Local authoring stays fast
76
76
  and self-contained, with no database or registry required.
77
77
  * **Registry mode.** A deployed, dynamic Next.js app backed by PostgreSQL serves
78
- a shared catalog. It only ever serves what a workspace published to it. The
78
+ a shared catalog from published records and packaged default pages. The
79
79
  registry never compiles or materializes workspace source.
80
80
 
81
81
  A **connected workspace** is workspace mode plus a registry connection. It still
82
- renders only your local files and still reads as `Workspace`, but it gains two
83
- commands: `publish` to push a locally built component or pattern up to a
84
- registry, and `checkout` to pull one back down into your workspace.
82
+ renders only local files and still reads as `Workspace`, but it can publish and
83
+ check out components, patterns, pages, token sets, and asset collections.
85
84
 
86
85
  Under the hood, documentation in every mode is consumed through one stable docs
87
86
  read API and one canonical artifact URL scheme (`/api/docs/artifacts/{path}`),
@@ -90,7 +89,7 @@ which store is behind it.
90
89
 
91
90
  ## Requirements
92
91
 
93
- * Node 18.17+
92
+ * Node 18.18 or newer. Node 20 LTS or newer is recommended.
94
93
  * npm 8+
95
94
  * A paid Figma account, if you fetch tokens from a Figma library
96
95
  * PostgreSQL (or a Postgres-compatible service such as Neon) for registry mode
@@ -104,13 +103,13 @@ npx handoff-app init
104
103
  ```
105
104
 
106
105
  The wizard asks for a project name, whether to include sample components, your
107
- preferred language (TypeScript or JavaScript), and optionally your Figma project
108
- ID and access token. It then creates the project directory, writes the config
109
- and starter files, and installs dependencies.
106
+ preferred language, optional Vercel deployment settings, and optional Figma
107
+ credentials. It then creates the project, writes the starter files, and installs
108
+ the project-local dependencies.
110
109
 
111
110
  ```bash
112
111
  cd my-handoff-project
113
- npm run fetch # pull design tokens from Figma (optional if no Figma creds yet)
112
+ npm run fetch # pull design tokens after adding Figma credentials (optional)
114
113
  npm run start # author locally with live preview at http://localhost:3000
115
114
  ```
116
115
 
@@ -121,8 +120,9 @@ The generated `package.json` wires up the common scripts:
121
120
  "scripts": {
122
121
  "start": "handoff-app start",
123
122
  "dev": "handoff-app dev",
123
+ "fetch": "handoff-app fetch",
124
124
  "build": "handoff-app build",
125
- "fetch": "handoff-app fetch"
125
+ "db:migrate": "handoff-app db:migrate"
126
126
  }
127
127
  }
128
128
  ```
@@ -143,18 +143,28 @@ my-handoff-project/
143
143
  │ ├─ Button.tsx # component implementation
144
144
  │ └─ button.handoff.ts # component declaration
145
145
  ├─ patterns/ # multi-component compositions
146
+ │ └─ hero/
147
+ │ └─ hero.handoff.ts
148
+ ├─ pages/ # custom Markdown documentation and navigation
149
+ │ └─ guides/
150
+ │ └─ getting-started.md
146
151
  ├─ sass/ # optional global SCSS entry
147
152
  ├─ js/ # optional global JS entry
148
- ├─ exported/ # fetched tokens and assets (commit these so clean/CI builds have data)
153
+ ├─ public/ # optional static assets
154
+ ├─ exported/ # fetched tokens and assets; commit this directory
149
155
  └─ out/ # build output (gitignored)
150
156
  ```
151
157
 
152
- ## Authoring components and patterns
158
+ A working Handoff project can mix global Sass and JavaScript entry points,
159
+ React, Handlebars, and CSF components, composed patterns, nested Markdown pages,
160
+ and fetched files under `exported/<projectId>`.
161
+
162
+ ## Authoring components, patterns, and pages
153
163
 
154
164
  A component is a directory with an implementation file and a declaration. The
155
- declaration is discovered as `*.handoff.{ts,js,cjs,json}` (the legacy
156
- `{dirname}.{js,cjs,json}` form keeps working too). List the directories you want
157
- built under `entries.components` in your config.
165
+ declaration is discovered as `*.handoff.{ts,js,cjs,json}`. The older
166
+ `{dirname}.{js,cjs,json}` form also works. List individual directories or a
167
+ parent collection such as `components` under `entries.components`.
158
168
 
159
169
  Declarations are written with the typed helpers exported from `handoff-app`:
160
170
 
@@ -192,6 +202,12 @@ directory name). The display `name`/`title` is independent of the `id`, so you
192
202
  can rename freely without breaking references. Identity is matched by `id`
193
203
  during publish and checkout.
194
204
 
205
+ Patterns use `definePattern` and compose component IDs. Each reference can select
206
+ a named preview or supply its own arguments. Custom documentation lives as
207
+ Markdown under `pages/`. A page's relative file path becomes both its route and
208
+ registry ID, so `pages/guides/getting-started.md` is served and transferred as
209
+ `guides/getting-started`.
210
+
195
211
  ## Fetching design tokens
196
212
 
197
213
  Handoff fetches tokens from Figma out of the box. Set your Figma credentials in
@@ -208,10 +224,16 @@ Then pull the latest foundations and component data:
208
224
  npm run fetch
209
225
  ```
210
226
 
211
- To get a personal access token in Figma, open the Figma menu, go to
212
- `Help and Account` then `Account Settings`, scroll to `Personal Access Token`,
213
- name a token, and copy it. Republish your Figma library after design changes and
214
- run `fetch` again to pick them up.
227
+ The fetch output is stored under `exported/<projectId>`. It includes
228
+ `tokens.json`, generated CSS and Sass, and available icon or logo archives. Keep
229
+ this directory in version control so local and CI builds use the same source
230
+ data.
231
+
232
+ To create a personal access token in Figma, open the account menu from the file
233
+ browser, select `Settings`, open `Security`, and generate a token under
234
+ `Personal access tokens`. Grant `file_content:read` and `library_content:read`,
235
+ then copy the token when Figma displays it. Republish your Figma library after
236
+ design changes and run `fetch` again to pick them up.
215
237
 
216
238
  ## Running locally
217
239
 
@@ -228,7 +250,7 @@ preview updates. The site boots at http://localhost:3000 by default.
228
250
  ## Building artifacts
229
251
 
230
252
  `handoff-app build` accepts two independent flags. `--target` decides *what* is
231
- produced — the target, not `NODE_ENV`, drives this. The optional `--package`
253
+ produced. The target, not `NODE_ENV`, drives this. The optional `--package`
232
254
  decides *how* that target is packaged for deployment.
233
255
 
234
256
  ### Static site (default)
@@ -238,12 +260,12 @@ handoff-app build # static target (default)
238
260
  handoff-app build --target static # explicit, identical output
239
261
  ```
240
262
 
241
- The static target builds your workspace components, patterns, and tokens,
242
- materializes the docs read model into route-shaped static files, and runs a
243
- static export to `out/<projectId>` (respecting `sitesOutputDirectory`). The
244
- output is plain static files: it renders list, detail, preview, and inspect
245
- pages and loads previews through canonical artifact URLs with no live server, so
246
- any static file server or CDN can serve it with no host-specific rewrites.
263
+ The static target builds workspace components and patterns, includes local
264
+ pages, tokens, and assets, materializes the docs read model into route-shaped
265
+ static files, and exports to `out/<projectId>` (respecting
266
+ `sitesOutputDirectory`). The output renders list, detail, preview, and inspect
267
+ pages with no live server, so any static file server or CDN can serve it without
268
+ host-specific rewrites.
247
269
 
248
270
  The build fails clearly if a required artifact referenced by generated HTML
249
271
  cannot be materialized, and it fails if the runtime is registry-only (there is
@@ -261,12 +283,15 @@ standalone bundle. It never builds or bundles workspace source and has no
261
283
  workspace-source runtime dependency. It serves the docs read API and the
262
284
  registry API from the database and published artifacts.
263
285
 
264
- The bundle is self-hostable on a Node server, VPS, or container. Start it with
265
- `node server.js` from the output directory and supply the database connection at
266
- deploy time:
286
+ The bundle is self-hostable on a Node server, VPS, or container. Supply the
287
+ database connection, a session secret of at least 32 characters, and the public
288
+ registry URL at deploy time:
267
289
 
268
290
  ```bash
269
- DATABASE_URL="postgres://…" node out/registry/server.js
291
+ DATABASE_URL="postgres://..." \
292
+ AUTH_SECRET="replace-with-a-long-random-value" \
293
+ AUTH_URL="https://registry.example.com" \
294
+ node out/registry/server.js
270
295
  ```
271
296
 
272
297
  A generated `README.md` next to the bundle documents the required env vars and
@@ -277,11 +302,15 @@ step (e.g. in CI/CD) before deploying:
277
302
  handoff-app db:migrate
278
303
  ```
279
304
 
280
- `db:migrate` reads your project config and DB env vars, applies the
305
+ `db:migrate` reads your project config and database environment variables, applies the
281
306
  package-owned migration set, and runs independently of `build`. PostgreSQL is
282
307
  the supported database; `runtime.registry.database.driver` selects the
283
- connection driver (`pg` or `neon`) — how to connect, not the database engine —
284
- and both drivers ship with the package.
308
+ connection driver (`pg` or `neon`), not the database engine. Both drivers ship
309
+ with the package.
310
+
311
+ Registry assets are stored in PostgreSQL by default, with a 4 MB limit per
312
+ blob. Larger deployments can use the built-in Vercel Blob adapter or a custom
313
+ storage adapter through `runtime.registry.assetStorage`.
285
314
 
286
315
  ### Deploying
287
316
 
@@ -289,8 +318,8 @@ and both drivers ship with the package.
289
318
  static file server or CDN. Commit `exported/` so a clean checkout has token
290
319
  data to build from.
291
320
  * **Registry** produces a self-contained Node bundle under `out/registry`. Run it
292
- on any Node host with `node out/registry/server.js` and a `DATABASE_URL` in the
293
- environment.
321
+ on any Node host with `node out/registry/server.js` and configure
322
+ `DATABASE_URL`, `AUTH_SECRET`, and `AUTH_URL` in the environment.
294
323
 
295
324
  The optional `--package` flag repackages a target without changing what it
296
325
  builds:
@@ -305,8 +334,15 @@ its `buildCommand` to change the target.
305
334
 
306
335
  ## Publishing and checkout
307
336
 
308
- Connect a workspace to a registry by adding a `registryConnection` block to your
309
- config and setting the access token env var:
337
+ For interactive use, authorize the workspace with the browser device flow:
338
+
339
+ ```bash
340
+ handoff-app login --url https://registry.example.com
341
+ ```
342
+
343
+ The saved login supplies the registry URL and credential to `publish` and
344
+ `checkout`. For CI or another headless environment, add a `registryConnection`
345
+ block and provide a user-issued access token through an environment variable:
310
346
 
311
347
  ```ts
312
348
  runtime: {
@@ -323,44 +359,37 @@ runtime: {
323
359
  HANDOFF_REGISTRY_ACCESS_TOKEN=your-registry-token
324
360
  ```
325
361
 
326
- Then push and pull entities by `id`:
362
+ Then push and pull one entity by ID, or omit the ID to process every entity of
363
+ that kind:
327
364
 
328
365
  ```bash
329
366
  handoff-app publish components button # build locally, upload that entity's package
330
367
  handoff-app checkout patterns hero # pull a registry entity into this workspace
368
+ handoff-app publish pages # publish every custom Markdown page
369
+ handoff-app checkout tokens foundation/colors
331
370
  ```
332
371
 
333
- `publish` runs a fresh targeted build of the selected entity and uploads only
334
- that entity's package: normalized metadata, source files referenced by its
335
- `entries`, rendered docs artifacts (including required shared artifacts like
336
- `component/main.css` and `component/main.js`), and build metadata. It fails
337
- before upload if required artifacts are missing, stale, or incompatible, and it
338
- never touches unrelated entities' artifacts. Declaration files are never
339
- uploaded.
340
-
341
- `checkout` reads the registry record and source files, writes them in standard
342
- authoring form, and synthesizes a local declaration in your configured
343
- `runtime.workspace.declarationFormat` (defaulting to `js`). Declarations are a
344
- workspace-only concern, so they are always synthesized locally and never read
345
- from the registry. Overwriting existing local files requires `--force` or an
346
- interactive confirmation.
372
+ `publish` builds the selected content before upload. Components and patterns
373
+ include normalized metadata, referenced source files, rendered docs artifacts,
374
+ shared preview assets, and build metadata. Pages upload their Markdown source.
375
+ Tokens and assets upload their generated sets or collections. Declaration files
376
+ remain local to the workspace.
377
+
378
+ `checkout` restores the standard workspace layout. It synthesizes component and
379
+ pattern declarations in `runtime.workspace.declarationFormat`, which defaults to
380
+ `js`, and restores pages, tokens, and assets to their normal locations.
381
+ Overwriting local files requires `--force` or interactive confirmation.
347
382
 
348
383
  ## Configuration
349
384
 
350
- Project configuration lives in `handoff.config.{ts,js,cjs,json}` (resolved in
351
- that precedence order). TypeScript and ESM configs use `defineConfig` for typed
352
- authoring; plain CommonJS configs that never call `defineConfig` keep working
353
- unchanged.
385
+ Project configuration lives in `handoff.config.{ts,js,cjs,json}` and is resolved
386
+ in that order. Use `defineConfig` for typed authoring and camelCase aliases.
387
+ Plain object exports remain supported.
354
388
 
355
389
  ```ts
356
390
  import { defineConfig } from 'handoff-app';
357
391
 
358
392
  export default defineConfig({
359
- app: {
360
- title: 'Acme Design System',
361
- client: 'Acme Corp',
362
- basePath: '',
363
- },
364
393
  entries: {
365
394
  scss: './sass/main.scss',
366
395
  js: './js/main.js',
@@ -394,13 +423,16 @@ runtime: {
394
423
  // Registry deployment settings (build --target registry, db:migrate).
395
424
  registry: {
396
425
  databaseUrlEnv: 'DATABASE_URL',
397
- apiTokenEnv: 'HANDOFF_REGISTRY_API_TOKEN',
398
426
  database: { driver: 'pg' }, // connection driver: 'pg' | 'neon' (PostgreSQL only)
427
+ assetStorage: {
428
+ adapter: 'database', // 'database' | 'vercel-blob' | 'custom'
429
+ maxInlineBytes: 4194304,
430
+ },
399
431
  },
400
432
  },
401
433
  ```
402
434
 
403
- Secrets are always referenced by environment-variable name, never written as
435
+ Registry secrets are referenced by environment-variable name, never written as
404
436
  values, and are redacted from API responses.
405
437
 
406
438
  ### Environment variables
@@ -410,9 +442,39 @@ values, and are redacted from API responses.
410
442
  | `HANDOFF_FIGMA_PROJECT_ID` | Figma file ID used by `fetch` |
411
443
  | `HANDOFF_DEV_ACCESS_TOKEN` | Figma personal access token used by `fetch` |
412
444
  | `HANDOFF_REGISTRY_URL` | Default env var for a connected workspace's registry URL |
413
- | `HANDOFF_REGISTRY_ACCESS_TOKEN` | Connected-workspace access token for publish/checkout |
445
+ | `HANDOFF_REGISTRY_ACCESS_TOKEN` | User-issued CI token for publish/checkout |
414
446
  | `DATABASE_URL` | Registry database connection string (name is configurable) |
415
- | `HANDOFF_REGISTRY_API_TOKEN` | Bearer token guarding registry management mutations |
447
+ | `AUTH_SECRET` | Long random secret used to sign registry browser sessions |
448
+ | `AUTH_URL` | Canonical public registry URL, including its base path |
449
+ | `RESEND_API_KEY` | Optional Resend API key for invitations and password resets |
450
+ | `AUTH_FROM_EMAIL` | Optional verified sender used with Resend |
451
+ | `BLOB_READ_WRITE_TOKEN` | Default credential for the Vercel Blob asset adapter |
452
+ | `HANDOFF_OUTPUT_DIR` | Override the fetched output directory (`exported`) |
453
+ | `HANDOFF_SITES_DIR` | Override the build output directory (`out`) |
454
+ | `HANDOFF_APP_PORT` | Local documentation server port (`3000`) |
455
+ | `HANDOFF_WEBSOCKET_PORT` | Local reload server port (`3001`) |
456
+ | `HANDOFF_LOG_LEVEL` | Logging level: `debug`, `info`, `warn`, `error`, or `silent` |
457
+
458
+ ### Registry installation and access
459
+
460
+ Database migrations remain an explicit deployment step. Configure the
461
+ environment, run `handoff-app db:migrate`, deploy, and immediately open
462
+ `/install` to create the first administrator. The installer validates the
463
+ deployment but never changes the schema.
464
+
465
+ The first visitor can claim an uninstalled registry. Do not leave a new
466
+ deployment unattended before installation is complete.
467
+
468
+ Administrators can invite users, manage roles, and deactivate accounts. Members
469
+ receive read access for browsing and checkout, while administrators can also
470
+ publish. If Resend is not configured, the registry shows invitation links once
471
+ for manual delivery.
472
+
473
+ `handoff-app login` stores a revocable credential in
474
+ `.handoff/cli-auth.json` and only uses it for the exact registry URL that issued
475
+ it. For CI, create a scoped token in Account settings and expose it through
476
+ `runtime.registryConnection.accessTokenEnv`. The former
477
+ `HANDOFF_REGISTRY_API_TOKEN` server secret is no longer accepted.
416
478
 
417
479
  ### Hooks
418
480
 
@@ -430,12 +492,16 @@ See [docs/api.md](docs/api.md#hooks) for hook arguments and examples.
430
492
  | --- | --- |
431
493
  | `init` | Scaffold a new workspace project with the interactive wizard |
432
494
  | `fetch` | Fetch design tokens from Figma |
495
+ | `scaffold` | Scaffold component stubs from fetched Figma components |
433
496
  | `dev` | Start the local dev server with the live docs read API |
434
497
  | `start` | Start the dev server with file watchers for live authoring |
435
498
  | `build [--target static\|registry] [--package standalone\|vercel]` | Build the static site (default) or registry app; `--package vercel` emits a Vercel Build Output bundle |
499
+ | `build:app` | Alias for `build --target static` |
436
500
  | `build:components [component]` | Build components without building the full app |
437
501
  | `publish <components\|patterns\|pages\|tokens\|assets> [id]` | Build and publish entities to the connected registry (all of the kind, or one by id) |
438
502
  | `checkout <components\|patterns\|pages\|tokens\|assets> [id]` | Pull entities from the connected registry into this workspace |
503
+ | `login --url <registry-url>` | Authorize this workspace through the registry device flow |
504
+ | `logout [--url <registry-url>]` | Revoke and remove the saved workspace credential |
439
505
  | `db:migrate` | Run registry database migrations (Drizzle / PostgreSQL) |
440
506
  | `make:component <name>` | Scaffold a new component |
441
507
  | `make:page <name> [parent]` | Scaffold a documentation page |
@@ -446,17 +512,8 @@ See [docs/api.md](docs/api.md#hooks) for hook arguments and examples.
446
512
  | `validate:components` | Run component validation |
447
513
 
448
514
  Global flags: `-c, --config <file>`, `-d, --debug`, `-f, --force`,
449
- `-h, --help`, `-v, --version`. See [docs/cli.md](docs/cli.md) for the full
450
- reference.
451
-
452
- ## Further reading
453
-
454
- * [Configure your project](https://www.handoff.com/docs/customization)
455
- * [Customize the content](https://www.handoff.com/docs/customization/content)
456
- * [Integrate tokens with your project](https://www.handoff.com/docs/tokens/integration)
457
- * [Build to static assets](https://www.handoff.com/docs/tokens/publishing)
458
- * [GitHub Actions CI/CD](https://www.handoff.com/docs/infrastructure/github/)
459
- * [Bitbucket Pipelines CI/CD](https://www.handoff.com/docs/infrastructure/bitbucket/)
515
+ `-h, --help`, `-v, --version`. Run `handoff-app <command> --help` for the exact
516
+ options in your installed version.
460
517
 
461
518
  ## Maintainers
462
519
 
@@ -218,7 +218,6 @@ const initializeProjectApp = (handoff_1, ...args_1) => __awaiter(void 0, [handof
218
218
  const escapedRuntimeMode = escapeForSingleQuotedJsString(runtimeMode);
219
219
  const escapedRegistryDriver = escapeForSingleQuotedJsString((0, driver_1.resolveRegistryDriver)(handoff.config));
220
220
  const escapedDatabaseUrlEnv = escapeForSingleQuotedJsString((0, driver_1.resolveDatabaseUrlEnv)(handoff.config));
221
- const escapedApiTokenEnv = escapeForSingleQuotedJsString((0, driver_1.resolveApiTokenEnv)(handoff.config));
222
221
  // Asset storage selection baked (provider + module + env-var names + non-secret options JSON).
223
222
  const assetStorage = (0, resolve_1.resolveAssetStorageFromConfig)(handoff.config);
224
223
  const escapedAssetStorageAdapter = escapeForSingleQuotedJsString(assetStorage.adapterKind);
@@ -236,7 +235,6 @@ const initializeProjectApp = (handoff_1, ...args_1) => __awaiter(void 0, [handof
236
235
  '%HANDOFF_RUNTIME_MODE%': escapedRuntimeMode,
237
236
  '%HANDOFF_REGISTRY_DRIVER%': escapedRegistryDriver,
238
237
  '%HANDOFF_REGISTRY_DATABASE_URL_ENV%': escapedDatabaseUrlEnv,
239
- '%HANDOFF_REGISTRY_API_TOKEN_ENV%': escapedApiTokenEnv,
240
238
  '%HANDOFF_ASSET_STORAGE_ADAPTER%': escapedAssetStorageAdapter,
241
239
  '%HANDOFF_ASSET_STORAGE_MODULE%': escapedAssetStorageModule,
242
240
  '%HANDOFF_ASSET_STORAGE_TOKEN_ENV%': escapedAssetStorageTokenEnv,
@@ -351,7 +349,11 @@ and \`.next/static/\` already copied alongside so the server serves them.
351
349
  ## Required environment variables (supplied at deploy time, never baked in)
352
350
 
353
351
  - \`${databaseUrlEnv}\` — PostgreSQL/Neon connection string, read at request time.
354
- - \`HANDOFF_REGISTRY_API_TOKEN\` — bearer token for registry management mutations (if used).
352
+ - \`AUTH_SECRET\` — a long, random secret used to sign browser sessions.
353
+ - \`AUTH_URL\` — the canonical public registry URL, including the configured base path.
354
+
355
+ Optional email delivery uses \`RESEND_API_KEY\` and \`AUTH_FROM_EMAIL\`. Without them, invitation
356
+ links are shown once to an administrator for manual delivery.
355
357
 
356
358
  ## Database migrations
357
359
 
@@ -366,6 +368,21 @@ ${databaseUrlEnv}="postgres://…" handoff-app db:migrate
366
368
  built with, and applies the package-owned migration set. It runs independently of \`build\` and of
367
369
  starting the server, so run it as a release/one-shot job.
368
370
 
371
+ ## Complete the one-time installation
372
+
373
+ After migrations and deployment, open \`/install\` immediately and create the initial administrator.
374
+ The installer verifies the deployment but never changes the schema. An exposed, uninstalled registry
375
+ can be claimed by its first visitor, so do not leave this step unattended.
376
+
377
+ Once installation completes, sign in and authorize a workspace:
378
+
379
+ \`\`\`bash
380
+ handoff-app login --url https://registry.example.com
381
+ \`\`\`
382
+
383
+ The browser approval issues a revocable, user-owned credential. The former
384
+ \`HANDOFF_REGISTRY_API_TOKEN\` fixed server secret is no longer accepted.
385
+
369
386
  ## Run it (self-hosting)
370
387
 
371
388
  Standalone is the documented artifact for self-hosting on a Node.js server, VPS, Docker, or
@@ -414,7 +431,7 @@ containers, custom Node servers, and other non-Vercel hosts.
414
431
  */
415
432
  const getRequiredRegistryRuntimeModules = (handoff) => {
416
433
  var _a;
417
- const base = ['next', 'react', 'react-dom', 'drizzle-orm'];
434
+ const base = ['next', 'next-auth', 'react', 'react-dom', 'drizzle-orm'];
418
435
  const driver = (0, driver_1.resolveRegistryDriver)(handoff.config);
419
436
  const driverModules = driver === 'neon' ? ['@neondatabase/serverless', 'ws'] : ['pg'];
420
437
  // Asset-storage SDKs the deployed registry must be able to load at request time. The pre-packaged
@@ -17,6 +17,7 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const config_1 = require("../config");
19
19
  const resolve_1 = require("../registry/asset-storage/resolve");
20
+ const connection_1 = require("../registry/connection");
20
21
  const driver_1 = require("../registry/db/driver");
21
22
  const paths_1 = require("./paths");
22
23
  /**
@@ -67,7 +68,6 @@ const buildServerRuntimeConfig = (config, modeOverride) => {
67
68
  registry: {
68
69
  driver: (0, driver_1.resolveRegistryDriver)(config),
69
70
  databaseUrlEnv: (0, driver_1.resolveDatabaseUrlEnv)(config),
70
- apiTokenEnv: (0, driver_1.resolveApiTokenEnv)(config),
71
71
  },
72
72
  assetStorage: {
73
73
  adapter: assetStorage.adapterKind,
@@ -84,17 +84,18 @@ const buildServerRuntimeConfig = (config, modeOverride) => {
84
84
  * connection inputs from. Both are always written together so they never drift.
85
85
  */
86
86
  const persistClientConfig = (handoff_1, ...args_1) => __awaiter(void 0, [handoff_1, ...args_1], void 0, function* (handoff, options = {}) {
87
+ var _a;
87
88
  const appPath = (0, paths_1.getAppPath)(handoff);
88
89
  // Ensure directory exists
89
90
  yield fs_extra_1.default.ensureDir(appPath);
90
91
  const clientConfig = (0, config_1.getClientConfig)(handoff.config);
91
- if (options.runtimeModeOverride) {
92
- // A forced registry mode (the registry build target) is never a connected workspace, so the
93
- // baked client config must report `connected: false` even when packaged from a connected
94
- // workspace project.
95
- const connected = options.runtimeModeOverride === 'registry' ? false : clientConfig.runtime.connected;
96
- clientConfig.runtime = Object.assign(Object.assign({}, clientConfig.runtime), { mode: options.runtimeModeOverride, connected });
97
- }
92
+ const mode = (_a = options.runtimeModeOverride) !== null && _a !== void 0 ? _a : clientConfig.runtime.mode;
93
+ // A registry build target is never a connected workspace, so it must bake `connected: false`
94
+ // even when packaged from a connected project. Otherwise a workspace is connected whenever a
95
+ // registry URL resolves — from config/env or a saved device login — so `login` alone surfaces
96
+ // the publish/checkout UI without also requiring HANDOFF_REGISTRY_URL.
97
+ const connected = mode === 'registry' ? false : Boolean((yield (0, connection_1.resolveAuthenticatedRegistryConnection)(handoff.config, handoff.workingPath)).url);
98
+ clientConfig.runtime = Object.assign(Object.assign({}, clientConfig.runtime), { mode, connected });
98
99
  yield fs_extra_1.default.writeJson(path_1.default.resolve(appPath, 'client.config.json'), { config: clientConfig }, { spaces: 2 });
99
100
  yield fs_extra_1.default.writeJson(path_1.default.resolve(appPath, 'runtime.server.json'), buildServerRuntimeConfig(handoff.config, options.runtimeModeOverride), { spaces: 2 });
100
101
  });
@@ -0,0 +1,2 @@
1
+ /** Fire-and-forget opening of a URL in the system browser. The printed URL remains the fallback. */
2
+ export declare const tryOpenBrowser: (url: string) => void;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tryOpenBrowser = void 0;
4
+ const child_process_1 = require("child_process");
5
+ /** Fire-and-forget opening of a URL in the system browser. The printed URL remains the fallback. */
6
+ const tryOpenBrowser = (url) => {
7
+ try {
8
+ const { protocol } = new URL(url);
9
+ if (protocol !== 'http:' && protocol !== 'https:')
10
+ return;
11
+ }
12
+ catch (_a) {
13
+ return;
14
+ }
15
+ const platform = process.platform;
16
+ // rundll32 is a plain executable rather than a shell, so spawn hands it the URL as one
17
+ // untouched argument. `cmd /c start` would re-parse shell metacharacters in it.
18
+ const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'rundll32.exe' : 'xdg-open';
19
+ const args = platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
20
+ try {
21
+ const child = (0, child_process_1.spawn)(command, args, {
22
+ detached: true,
23
+ stdio: 'ignore',
24
+ windowsHide: platform === 'win32',
25
+ });
26
+ child.on('error', () => undefined);
27
+ child.unref();
28
+ }
29
+ catch (_b) {
30
+ // Headless and SSH environments can use the URL printed by the login flow.
31
+ }
32
+ };
33
+ exports.tryOpenBrowser = tryOpenBrowser;
@@ -0,0 +1,11 @@
1
+ import { type CliAuth } from './store';
2
+ export interface LoginOptions {
3
+ openBrowser?: boolean;
4
+ }
5
+ /** Complete the device grant and persist the one-time plaintext access token for this workspace. */
6
+ export declare const loginWithDevice: (workingPath: string, remoteUrl: string, options?: LoginOptions) => Promise<CliAuth>;
7
+ /**
8
+ * Best-effort remote revocation. The caller must clear the local credential regardless of failure.
9
+ * The bearer header identifies the current token while the body supports RFC 7009-style handlers.
10
+ */
11
+ export declare const revokeAccessToken: (auth: CliAuth) => Promise<void>;