@openparachute/app 0.2.0-rc.10

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.
@@ -0,0 +1,62 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://parachute.computer/schemas/app-config.json",
4
+ "title": "parachute-app config",
5
+ "description": "Runtime configuration for parachute-app. Persisted at $PARACHUTE_HOME/app/config.json.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "hub_url": {
10
+ "type": "string",
11
+ "format": "uri",
12
+ "default": "http://127.0.0.1:1939",
13
+ "description": "Hub origin for DCR registration."
14
+ },
15
+ "auto_register_oauth_clients": {
16
+ "type": "boolean",
17
+ "default": true,
18
+ "description": "Auto-register each added UI as an OAuth client with hub."
19
+ },
20
+ "disabled": {
21
+ "type": "boolean",
22
+ "default": false,
23
+ "description": "Stop hosting all UIs. Useful for maintenance."
24
+ },
25
+ "default_scope_required": {
26
+ "type": "array",
27
+ "items": { "type": "string" },
28
+ "default": ["vault:*:read"],
29
+ "description": "Fallback OAuth scopes when a UI's meta.json doesn't declare any."
30
+ },
31
+ "dev_mode_allowed": {
32
+ "type": "boolean",
33
+ "default": true,
34
+ "description": "Whether `parachute-app dev <name>` is permitted."
35
+ },
36
+ "bootstrap_default_apps": {
37
+ "type": "object",
38
+ "additionalProperties": false,
39
+ "default": { "enabled": true, "apps": ["@openparachute/notes-ui"] },
40
+ "description": "On first boot (when uis/ is empty), apps auto-installs these npm packages so operators get something working out of the box. Set enabled=false to skip; set apps=[] to disable without flipping the toggle.",
41
+ "properties": {
42
+ "enabled": {
43
+ "type": "boolean",
44
+ "default": true,
45
+ "description": "If true, bootstrap default apps on first boot when uis/ is empty."
46
+ },
47
+ "apps": {
48
+ "type": "array",
49
+ "items": { "type": "string" },
50
+ "default": ["@openparachute/notes-ui"],
51
+ "description": "npm package specifiers to install as default apps on first boot."
52
+ }
53
+ }
54
+ },
55
+ "auto_provision_required_schema": {
56
+ "type": "boolean",
57
+ "default": true,
58
+ "description": "When a UI's meta.json declares required_schema, auto-provision its tags in vault on add. Best-effort: errors log + warn but don't fail the install. See patterns#57."
59
+ }
60
+ },
61
+ "x-scopes": ["app:read", "app:admin"]
62
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "parachute-app",
3
+ "displayName": "App",
4
+ "tagline": "Host module for custom Parachute UIs — drop a built bundle in and serve it under one origin.",
5
+ "version": "0.2.0-rc.2",
6
+ "capabilities": [
7
+ "ui-host",
8
+ "oauth-client-registration",
9
+ "admin-spa",
10
+ "dev-mode-sse",
11
+ "bootstrap-default-apps",
12
+ "auto-provision-schema"
13
+ ]
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "app",
3
+ "manifestName": "parachute-app",
4
+ "displayName": "App",
5
+ "tagline": "Host module for custom Parachute UIs — drop a built bundle in and serve it under one origin.",
6
+ "port": 1946,
7
+ "paths": ["/app", "/.parachute"],
8
+ "stripPrefix": false,
9
+ "health": "/app/healthz",
10
+ "uiUrl": "/app/admin/",
11
+ "managementUrl": "/app/admin/",
12
+ "startCmd": ["parachute-app", "serve"],
13
+ "scopes": { "defines": ["app:read", "app:admin"] }
14
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,537 @@
1
+ # Changelog
2
+
3
+ This file tracks the workspace's two npm-publishable packages
4
+ side-by-side:
5
+
6
+ - `@openparachute/app` (host module, lives in `packages/app-host/`)
7
+ - `@openparachute/app-client` (shared client library, lives in `packages/app-client/`)
8
+
9
+ The admin SPA at `web/admin/` ships inside the host package as
10
+ `dist/admin/`; its version mirrors the host's version.
11
+
12
+ ## [app 0.2.0-rc.10] - 2026-05-23
13
+
14
+ ### Removed
15
+
16
+ - Dropped `kind` field from `packages/app-host/.parachute/module.json`. Hub's validator made it optional in hub#327; this PR completes the cleanup per hub#301 Phase B. No behavior change — app was never branched-on by kind. Closes part of hub#330.
17
+
18
+ ## [app 0.2.0-rc.9] - 2026-05-23
19
+
20
+ ### Added
21
+
22
+ - `TagSchemaDeclaration.parent_names: string[]` — apps' `required_schema.tags[]` can now declare parent tag relationships for hierarchical schemas (e.g. `capture/text` with `parent_names: ["capture"]`). Phase 2.0 just validates the shape; Phase 2.1+ auto-provisioner will use it to mint parent-child relationships in vault. Closes [parachute-app#19](https://github.com/ParachuteComputer/parachute-app/issues/19).
23
+
24
+ ## [app 0.2.0-rc.8] - 2026-05-23
25
+
26
+ ### Added
27
+
28
+ - Inject runtime tenancy contract (`<base href>` + `<meta name="parachute-mount">` + `<meta name="parachute-hub">`) into served index.html for hosted UIs. Implements the host side of the runtime-tenancy-contract pattern (closes [parachute-app#21](https://github.com/ParachuteComputer/parachute-app/issues/21)). The `<base href>` resolves the trailing-slash gotcha — `/app/<name>` (no slash) now works because relative asset URLs resolve correctly. The meta tags are read by `@openparachute/app-client`'s helpers (forthcoming, [parachute-app#22](https://github.com/ParachuteComputer/parachute-app/issues/22)).
29
+ - `src/tenancy-injection.ts` — string-scan injector with regex-based idempotency marker (`<meta name="parachute-mount">`). Insertion point is immediately after `<head>` so the injected `<base href>` wins over any later `<base>` per HTML's first-base-wins rule. No-`<head>` documents serve unmodified with a warning log.
30
+ - `src/__tests__/tenancy-injection.test.ts` (18 tests) — happy path, idempotency (incl. single-quoted attributes), no-`<head>` skip, HTML attribute escaping for `&` / `<` / `>` / `"`, custom mount slugs, https hub origins.
31
+ - 8 integration tests in `src/__tests__/http-server.test.ts` under "HTTP — runtime tenancy contract injection" — root document, no-trailing-slash, custom slug, SPA-fallback path, idempotency, no-`<head>` passthrough, `PARACHUTE_HUB_ORIGIN` env override, non-index-asset regression guard.
32
+
33
+ ### Changed
34
+
35
+ - `serveFileWithHeaders` now accepts `hubOrigin?: string` and a logger override. When the served filename is `index.html`, it runs the tenancy-contract pass (always-on when `hubOrigin` is supplied) followed by the dev-mode reload-script pass (when dev mode is on). Both passes are idempotent string-scans.
36
+ - `serveUiAsset` resolves the hub origin per-request via the existing `getHubOrigin(state.config.hub_url)` from `auth.ts` — `PARACHUTE_HUB_ORIGIN` env var takes precedence, then `config.hub_url`, then `http://127.0.0.1:1939` loopback fallback. Reuses the same resolution path the JWT validator already uses; no new config field or env var introduced.
37
+
38
+ ### Deferred (out of scope for parachute-app#21)
39
+
40
+ - `<meta name="parachute-vault">` — vault-binding-via-session needs a separate design pass.
41
+ - `<meta name="parachute-tenant-id">` — derivable on the consumer side from `parachute-mount`.
42
+ - `<meta name="parachute-vault-origin">` — forward-looking for cross-origin vault.
43
+
44
+ ### Verified
45
+
46
+ | Suite | Before | After |
47
+ |---|---|---|
48
+ | `bun test packages/app-host/src/` | 367 / 0 | 393 / 0 |
49
+
50
+ Typecheck clean. Biome clean.
51
+
52
+ ## [app 0.2.0-rc.7] - 2026-05-23
53
+
54
+ fix(app): SPA-fallback only for navigation requests — file-extension
55
+ asset misses (`.js`, `.css`, `.webmanifest`, etc.) now correctly return
56
+ 404 instead of serving the SPA shell.
57
+
58
+ ### The bug
59
+
60
+ `serveUiAsset` fell back to `dist/index.html` on every miss, including
61
+ requests for assets. The browser then tried to parse the HTML shell as
62
+ JS / a PWA manifest / etc., producing confusing errors that masked the
63
+ real cause (a missing or misnamed asset):
64
+
65
+ ```
66
+ manifest.webmanifest: Manifest: Line: 1, column: 1, Syntax error.
67
+ <chunk>.js: Failed to load module script: Expected JavaScript-or-Wasm
68
+ module, got "text/html"
69
+ ```
70
+
71
+ Operators on notes-ui installs hit this when the PWA manifest was
72
+ missing or a code-split chunk failed to resolve.
73
+
74
+ ### Fix
75
+
76
+ A file-extension heuristic now classifies each miss as either an asset
77
+ request (known static extensions: `.js`, `.mjs`, `.cjs`, `.css`,
78
+ `.json`, `.webmanifest`, `.map`, image / font / media types, `.wasm`,
79
+ `.txt`) or a navigation request (no extension, or `.html`). Asset
80
+ misses → 404 with body `"Not Found"`. Navigation misses → SPA shell
81
+ with no-cache headers, unchanged. The check fires at every SPA-fallback
82
+ point in `serveUiAsset` (the deletion-race fallback, the traversal-guard
83
+ fallback, and the primary miss branch) for defense in depth — a
84
+ traversal attempt with an asset-shaped suffix (`../etc/passwd.txt`) is
85
+ now 404'd instead of returning HTML.
86
+
87
+ The file-existence-→-serve happy path is unchanged; only the miss
88
+ branch is affected.
89
+
90
+ ### Verified
91
+
92
+ | Suite | Before | After |
93
+ |---|---|---|
94
+ | `bun test packages/app-host/src/` | 359 / 0 | 367 / 0 |
95
+
96
+ Typecheck clean. The added tests cover: missing `.js` → 404, missing
97
+ `.webmanifest` → 404, missing `.css` → 404, missing route with no
98
+ extension → SPA shell, missing `.html` route → SPA shell, bare-segment
99
+ route → SPA shell, present `.js` asset still served (regression
100
+ guard), and the existing traversal test split into asset-shaped (now
101
+ 404) vs no-extension (still SPA fallback).
102
+
103
+ ## [app 0.2.0-rc.6] - 2026-05-22
104
+
105
+ fix(app): correct `kind` to `"api"` — app is a backend that proxies,
106
+ not a static-served frontend (folds the in-flight rc.6 per
107
+ [app#14](https://github.com/ParachuteComputer/parachute-app/issues/14)).
108
+
109
+ The initial rc.6 in-flight version carried `"kind": "frontend"` to
110
+ unblock the hub validator (which at rc.13 still required the field).
111
+ That was the wrong value semantically. App is a **backend** that
112
+ serves UI bundles via its own HTTP server — hub's `/app/*` proxy
113
+ forwards to app on `:1946`, then app's HTTP layer serves the admin
114
+ SPA + `notes-ui` + any installed sub-units. Hub does NOT static-serve
115
+ from app's `dist/`; the `"frontend"` framing was inaccurate and
116
+ risked future tooling that branches on `kind === "frontend"` (already
117
+ in `parachute-hub/src/commands/upgrade.ts:376` — which runs
118
+ `bun run build` for kind-frontend modules) treating app as a
119
+ static-bundle module and breaking the runtime HTTP layer.
120
+
121
+ `"api"` is the accurate value: app's role is the backend-proxy lane,
122
+ same as vault / scribe / runner. With hub#327 landing alongside this
123
+ PR — the validator no longer inspects `kind` at all — future app
124
+ releases can drop the field entirely. For now keeping it explicit
125
+ works under both the old validator (rc.13 strict-require) and the
126
+ new (rc.14+ no-validate); safest immediate fix that doesn't gate on
127
+ hub-rc.14 propagation.
128
+
129
+ ## [app 0.2.0-rc.5] - 2026-05-22
130
+
131
+ fix(app): self-register uses `manifestName` as services.json row key
132
+ (matches hub install path; closes duplicate-port bug).
133
+
134
+ Hub installs modules under `manifest.manifestName` (`"parachute-app"`),
135
+ but the boot-time self-registration was writing under the short name
136
+ `"app"`. The two writes left services.json with two rows on the same
137
+ port, which trips hub's duplicate-port detector on re-read
138
+ (`duplicate port 1946 — claimed by both "parachute-app" and "app"`).
139
+
140
+ The row key is now sourced from `.parachute/module.json#manifestName`,
141
+ so the install path and the runtime path converge to one row. Mirrors
142
+ the fix landed in parachute-runner.
143
+
144
+ ## [app 0.2.0-rc.1] + [app-client 0.1.0-rc.1] - 2026-05-21
145
+
146
+ feat(app): Phase 2.0 — extract `@openparachute/app-client` shared
147
+ library as a sub-package + add `required_schema` to meta.json
148
+ (folds [patterns#57](https://github.com/ParachuteComputer/parachute-patterns/issues/57)).
149
+
150
+ This is the monorepo-restructure release. The repo grows a workspace
151
+ shape with two publishable packages and a workspace-only admin SPA.
152
+ Each hosted app today re-implements OAuth + vault REST + token storage
153
+ from scratch (Notes did this; the Gitcoin Brain UI has its own); the
154
+ new `@openparachute/app-client` package extracts the canonical pattern.
155
+
156
+ Reference: [design doc 2026-05-21-parachute-apps-design.md](https://github.com/ParachuteComputer/parachute.computer/blob/main/design/2026-05-21-parachute-apps-design.md).
157
+
158
+ ### Monorepo restructure
159
+
160
+ - `packages/app-host/` — the host module (formerly the entire repo).
161
+ Bumped to `0.2.0-rc.1` (minor for the restructure).
162
+ - `packages/app-client/` — NEW shared library at `0.1.0-rc.1`.
163
+ - Root `package.json` becomes the workspace root (private
164
+ `@openparachute/app-monorepo`). Workspaces: `packages/*` + `web/admin`.
165
+ - `web/admin/` (admin SPA) unchanged in shape; build output redirected
166
+ to `packages/app-host/dist/admin/` so the daemon's `defaultAdminDir`
167
+ still resolves correctly. Bumped to `0.2.0-rc.1` to mirror the host.
168
+
169
+ ### `@openparachute/app-client` 0.1.0-rc.1 — public surface
170
+
171
+ Tree-shake-friendly subpath exports + a barrel:
172
+
173
+ | Subpath | Surface |
174
+ |---|---|
175
+ | `oauth` | `ParachuteOAuth` driver class (PKCE + same-hub auto-trust); `PendingApprovalError`, `RefreshHttpError`, `InsecureContextError` |
176
+ | `vault-client` | `VaultClient` REST client with auto-refresh on 401/403; `VaultAuthError` (carries `errorType` per notes#150), `VaultNotFoundError`, `VaultUnreachableError`, `VaultConflictError`, `VaultTargetExistsError`, `VaultUploadError` |
177
+ | `token-storage` | `loadToken` / `saveToken` / `clearToken` / `clearAllTokensForApp`; key format `parachute_token:<app-name>:<vault-scope>`; auto-prunes expired tokens that have no refresh_token |
178
+ | `sw-reload` | `reloadAfterServiceWorkerUpdate` (lifted from notes#148) |
179
+ | `vault-id` | `vaultIdFromUrl` + `normalizeVaultUrl` (notes#149 URL-drift fix) |
180
+
181
+ Notes-canonical implementation extracted with the following deltas:
182
+ - All paths handle the no-`window` / SSR case (token-storage falls back
183
+ to a `NULL_STORAGE` shim; sessionStorage in `ParachuteOAuth` likewise).
184
+ - Cursor pagination (`queryNotesCursor`) reads `X-Next-Cursor` and
185
+ preserves the cursor through the auth-retry path.
186
+ - `ParachuteOAuth.beginFlow` accepts a `vaultName` opt that adds the
187
+ `vault=<name>` hint to `/oauth/authorize` for the multi-vault
188
+ narrow-on-pick pattern Notes uses today.
189
+
190
+ ### `@openparachute/app` 0.2.0-rc.1 — meta.json `required_schema`
191
+
192
+ Per patterns#57 ("Surfaces declare required vault schema"), `meta.json`
193
+ gains an optional `required_schema` field:
194
+
195
+ ```json
196
+ {
197
+ "required_schema": {
198
+ "tags": [
199
+ {
200
+ "name": "capture",
201
+ "description": "Quick captures",
202
+ "fields": {
203
+ "source": { "type": "string", "required": true },
204
+ "createdAt": { "type": "date" }
205
+ }
206
+ }
207
+ ]
208
+ }
209
+ }
210
+ ```
211
+
212
+ Phase 2.0 scope: **validate + surface in admin SPA**. Phase 2.1+ will
213
+ auto-provision missing tag-identity rows in vault via
214
+ `VaultClient.updateTag` at install time; that's tracked separately.
215
+
216
+ The admin SPA's modules table grows a per-row "Schema requirements"
217
+ expandable summary; the per-UI info page renders the full declaration.
218
+
219
+ ### Verified
220
+
221
+ | Suite | Before | After |
222
+ |---|---|---|
223
+ | `bun test packages/app-host/src/` | 270 / 0 | 281 / 0 |
224
+ | `bun test packages/app-client/src/` | n/a | 80 / 0 |
225
+ | `cd web/admin && bun run test` | 31 / 0 | 40 / 0 |
226
+
227
+ Typecheck clean (`tsc --noEmit` across all three). Build clean
228
+ (`bun run build` from root builds app-client then app-host).
229
+
230
+ ---
231
+
232
+ ## [0.1.0-rc.4] - 2026-05-22
233
+
234
+ feat(app): Phase 1.3 — dev mode with SSE live-reload (closes Phase 1).
235
+
236
+ Phase 1.3 closes Phase 1 of parachute-app and resolves the recurring
237
+ "edit code, build, browser shows old" frustration tracked in
238
+ [parachute-notes#151](https://github.com/ParachuteComputer/parachute-notes/issues/151)
239
+ at the platform level. Adds operator-triggered dev mode: `parachute-app
240
+ dev <name>` flips a UI into a no-cache mode + injects an EventSource
241
+ shim into `index.html` that reloads the tab when the operator runs
242
+ `parachute-app dev <name> --trigger` after a rebuild. Reference:
243
+ [design doc section 18](https://github.com/ParachuteComputer/parachute.computer/blob/main/design/2026-05-21-parachute-apps-design.md#18-caching--reload-strategy).
244
+
245
+ ### Added
246
+
247
+ - `src/dev-mode.ts` — process-local, in-memory dev-mode state. One Map
248
+ for `name → { enabled, enabledAt, watchDir?, buildCmd? }`, one Map
249
+ for `name → Set<DevReloadSubscriber>`. Exports `enableDevMode`,
250
+ `disableDevMode`, `isDevMode`, `listDevMode`, `getDevMode`,
251
+ `addSubscriber`, `removeSubscriber`, `broadcastReload`,
252
+ `subscriberCount`, `closeAllSubscribers`, `resetDevMode`. Idempotent
253
+ enable preserves `enabledAt`; disable closes every connected SSE
254
+ stream so the next request resumes production cache headers cleanly.
255
+ - `src/dev-injection.ts` — HTML script-injection (string scan, no
256
+ cheerio dep). Inserts `<script id="parachute-app-dev-reload">` just
257
+ before `</head>`, with fallbacks (`before-script` → `after-body` →
258
+ `append`) for unusual document structures. Idempotent via the marker
259
+ id — re-rendering the same document doesn't duplicate the tag. The
260
+ script body opens an EventSource against `/app/<name>/_dev/reload`
261
+ and `window.location.reload()`s on `reload` events (200ms debounce).
262
+ - `src/dev-routes.ts` — Phase 1.3 HTTP endpoints:
263
+ - `GET /app/<name>/_dev/reload` (UNAUTHENTICATED) — SSE stream;
264
+ 404 when the UI isn't in dev mode. Emits a `: connected` keepalive
265
+ on accept; broadcasts `event: reload\ndata: {"timestamp": ...}` on
266
+ trigger. Disconnects clean up via the stream's `cancel` hook.
267
+ - `POST /app/<name>/dev/enable` (`app:admin`) — flip on. Honors
268
+ `config.dev_mode_allowed: false` with 409.
269
+ - `POST /app/<name>/dev/disable` (`app:admin`) — flip off + close
270
+ every subscriber.
271
+ - `POST /app/<name>/dev/trigger` (`app:admin`) — broadcast `reload`;
272
+ 409 when dev mode is off. Returns `{ notified: <count> }`.
273
+ - `GET /app/<name>/dev` (`app:read`) — per-UI status.
274
+ - `GET /app/dev/list` (`app:read`) — UIs currently in dev mode.
275
+ - `src/cache-headers.ts` — `cacheHeadersFor` takes a `devMode` boolean.
276
+ When true, every response is `no-cache, no-store, must-revalidate` —
277
+ overrides immutable on hashed assets AND `no-cache` on the PWA SW.
278
+ - `src/http-server.ts` — wires dev-routes ahead of admin routes; per-
279
+ request `isDevMode(meta.name)` check feeds both the cache headers
280
+ and the index.html injection. `serveFileWithHeaders` accepts a
281
+ `devMode` parameter; when true + filename is `index.html`, it parses
282
+ the body via `injectDevReloadScript` before responding. HEAD reports
283
+ the injected byte length.
284
+ - `src/index.ts` — re-exports the dev-mode + dev-injection surface,
285
+ exposes `routeDev` + `DevRoutesOpts`, replaces the Phase 1.3 stub
286
+ `setDevMode` with a real wrapper.
287
+ - `bin/parachute-app.ts` — replaces the Phase 1.3 stub with four
288
+ sub-verbs:
289
+ - `parachute-app dev <name>` — enable (idempotent)
290
+ - `parachute-app dev <name> --off` — disable
291
+ - `parachute-app dev <name> --trigger` — broadcast reload
292
+ - `parachute-app dev list` — show UIs currently in dev mode
293
+ Help text reflects the full Phase 1.3 verb set.
294
+ - `web/admin/src/lib/api.ts` — typed helpers: `enableDevMode`,
295
+ `disableDevMode`, `triggerReload`, `getDevModeStatus`, `listDevMode`.
296
+ - `web/admin/src/routes/Modules.tsx` — per-row "Dev" badge + "Enable
297
+ dev" / "Disable dev" / "Trigger reload" buttons. Refreshes the
298
+ dev-status map alongside the UI list.
299
+ - Tests:
300
+ - `src/__tests__/dev-mode.test.ts` (15 tests) — state, subscribers,
301
+ broadcast reaping.
302
+ - `src/__tests__/dev-injection.test.ts` (10 tests) — happy path +
303
+ idempotence + all three fallback branches + escape defense.
304
+ - `src/__tests__/dev-routes.test.ts` (14 tests) — every endpoint +
305
+ auth gates + SSE subscribe / broadcast / cancel.
306
+ - `src/__tests__/dev-integration.test.ts` (10 tests) — full
307
+ end-to-end via Bun.serve including script injection, no-cache
308
+ override, SSE broadcast, dev-list, HEAD content-length.
309
+ - `src/__tests__/cache-headers.test.ts` — extra coverage for the
310
+ `devMode` parameter.
311
+ - `src/__tests__/cli.test.ts` — refreshed for the new `dev` verbs.
312
+ - `web/admin/src/routes/Modules.test.tsx` — updated to mock the
313
+ `/app/dev/list` companion fetch + assert the new dev controls.
314
+
315
+ ### Changed
316
+
317
+ - Bumped to `0.1.0-rc.4`. `.parachute/info` capabilities now include
318
+ `dev-mode-sse`.
319
+ - HTTP server routing: dev-routes dispatcher fires ahead of admin-routes
320
+ so the per-UI `_dev/reload` path doesn't race with the admin matcher.
321
+ - `cacheHeadersFor` signature gains a third `devMode = false` parameter
322
+ (backwards-compatible — existing meta-less callers continue to work).
323
+ - Admin SPA's Modules table grew a "Dev" column; existing layout
324
+ preserved.
325
+
326
+ ### Verified
327
+
328
+ - `bun test src/` → 270 pass / 0 fail (was 213).
329
+ - `cd web/admin && bun run test` → 31 pass / 0 fail (was 21).
330
+ - `bun run typecheck` → clean (root + web/admin).
331
+ - `bunx biome check .` → clean.
332
+ - `bun run build` → `dist/admin/` populated.
333
+ - `bin/parachute-app.ts --version` → 0.1.0-rc.4.
334
+ - `bin/parachute-app.ts --help` → shows the four `dev` sub-verbs.
335
+
336
+ ## [0.1.0-rc.3] - 2026-05-21
337
+
338
+ feat(app): Phase 1.2 — admin endpoints + DCR + npm-fetch + Vite+React admin SPA.
339
+
340
+ Phase 1.2 takes the bundled-UI-host daemon from "operator manually drops
341
+ dist/ into uis/" to "operator runs `parachute-app add <source>` and the
342
+ daemon handles copy + DCR + re-scan." Adds the admin HTTP surface, the
343
+ Dynamic Client Registration call to hub, an npm-fetch shorthand for
344
+ sourcing UIs by package specifier, and a Vite + React admin SPA mounted
345
+ at `/app/admin/`.
346
+
347
+ ### Added
348
+
349
+ - `src/auth.ts` — hub-JWT validation via `@openparachute/scope-guard@^0.3.0`.
350
+ Audience `app`; scopes `app:read` (list/info) and `app:admin` (add/remove/
351
+ reload). `enforceScope` mirrors runner's pattern; `hasReadAccess` lets
352
+ admin imply read.
353
+ - `src/operator-token.ts` — operator bearer sourcing for outbound DCR
354
+ calls. Priority: `PARACHUTE_HUB_TOKEN` env > `~/.parachute/operator.token`
355
+ file (chmod 0o600 required on Unix). Missing token returns undefined; the
356
+ caller decides whether that's fatal.
357
+ - `src/dcr.ts` — RFC 7591 Dynamic Client Registration with hub. Sends
358
+ `client_name`, `redirect_uris` (`/app/<name>/` + `/app/<name>/oauth-callback`),
359
+ `scope` (joined), `token_endpoint_auth_method: "none"`, `grant_types:
360
+ ["authorization_code"]`, `response_types: ["code"]`. Persists the returned
361
+ `client_id` to `~/.parachute/app/uis/<name>/.oauth-client.json` (chmod 0o600).
362
+ Surfaces hub errors as a typed `DcrError` (status: hub_unreachable /
363
+ hub_rejected / invalid_response). Best-effort `DELETE /oauth/clients/<id>`
364
+ on remove; tolerates 404/405 (RFC 7592 not universally implemented yet).
365
+ - `src/npm-fetch.ts` — `bun add <spec>` into a `/tmp/parachute-app-staging-*`
366
+ dir, then copies `node_modules/<pkg>/dist/` into the UI's home. Distinguishes
367
+ 404 / network / generic errors by sniffing stderr. Cleanup always runs.
368
+ Supports plain names, scoped names, and `@version` tails.
369
+ - `src/admin-routes.ts` — the Phase 1.2 admin endpoints:
370
+ - `GET /app/list` (`app:read`) — serialized UI summaries + skipped list
371
+ - `POST /app/add` (`app:admin`) — accepts local path OR npm spec; copies
372
+ bundle + writes meta.json + (optionally) fires DCR + re-scans
373
+ - `DELETE /app/<name>` (`app:admin`) — revokes OAuth + removes dir +
374
+ re-scans
375
+ - `POST /app/<name>/reload` (`app:admin`) — re-scans without daemon restart
376
+ - `GET /app/<name>/info` (`app:read`) — full info: meta + oauth + paths
377
+ - `GET /app/<name>/oauth-client` — UNAUTHENTICATED — returns
378
+ `{client_id, hub_url, scope, redirect_uris}` for the UI to use at boot
379
+ - Auto-rejects `/app/admin` as a reserved mount path.
380
+ - Validates name + path patterns; rejects collisions with 409.
381
+ - After every mutation: re-runs `scanUis()` + refreshes `services.json`
382
+ with the per-UI `uis` map (design doc section 12 shape).
383
+ - `src/http-server.ts` — wires the admin routes into the existing Bun.serve
384
+ handler. POST/DELETE now flow through the admin matcher; non-admin POST/
385
+ DELETE returns 404 (was 405). Unknown methods still return 405. New
386
+ `/app/admin/[*]` static mount serves the built SPA from `dist/admin/`;
387
+ falls back to a dev-time placeholder when the bundle is absent.
388
+ - `web/admin/` — Vite + React + TypeScript admin SPA. React 19, react-router
389
+ 7. Routes: `/` (Modules), `/add` (Add UI form), `/info/:name`. Auth via
390
+ `localStorage["parachute_operator_token"]` (Phase 1.3 wires hub-session
391
+ auth). Builds to root `dist/admin/`. Per-UI Reload + Remove buttons hit
392
+ the live admin endpoints. Skipped UIs surface inline with their failure
393
+ reason.
394
+ - `bin/parachute-app.ts` — `add`, `remove`, `list`, `reload` verbs are no
395
+ longer stubs. Each calls the local daemon's admin endpoints over HTTP
396
+ (`PARACHUTE_APP_URL` env overrides). Sources the operator bearer via the
397
+ same `readOperatorToken` the daemon uses.
398
+ - Tests:
399
+ - `src/__tests__/auth.test.ts` — bearer extraction, scope checks,
400
+ `validateBearer` 401 paths, `getHubOrigin` resolution
401
+ - `src/__tests__/operator-token.test.ts` — env vs file priority, mode
402
+ 0o600 defense
403
+ - `src/__tests__/dcr.test.ts` — DCR request shape, operator-bearer
404
+ forwarding, hub-error surfacing, file persistence + revocation
405
+ - `src/__tests__/npm-fetch.test.ts` — spec parsing, fake-bun-add
406
+ integration, error-code mapping
407
+ - `src/__tests__/admin-routes.test.ts` — auth gates + full happy paths
408
+ with the `enforceScopeFn` test seam
409
+ - `src/__tests__/admin-integration.test.ts` — end-to-end add/delete/
410
+ reload through Bun.serve
411
+ - `web/admin/src/lib/api.test.ts` — api.ts wrapper coverage
412
+ - `web/admin/src/routes/Modules.test.tsx` — list view, error banner,
413
+ Reload + Remove button flows
414
+ - `web/admin/src/routes/Add.test.tsx` — form submission shape + success
415
+ rendering
416
+ - `web/admin/src/App.test.tsx` — shell + token banner
417
+
418
+ ### Changed
419
+
420
+ - Bumped to `0.1.0-rc.3`. `.parachute/info` capabilities now include
421
+ `admin-spa`.
422
+ - `bin/parachute-app.ts` help text reflects the live `add`/`remove`/`list`/
423
+ `reload` verbs.
424
+ - `src/http-server.ts` 405 policy: POST/DELETE no longer return 405 globally;
425
+ they flow to admin routes and fall through to 404 when no admin route
426
+ matches. PATCH and other unhandled methods still return 405.
427
+ - `package.json#files` now includes `dist/admin/**` so the npm-published
428
+ bundle ships the admin SPA. Added `build` / `test:admin` / `typecheck:all`
429
+ scripts coordinating root + web/admin.
430
+ - `package.json` now depends on `@openparachute/scope-guard@^0.3.0` for
431
+ hub-JWT validation.
432
+
433
+ ### Verified
434
+
435
+ - `bun test ./src` → 213 pass / 0 fail (was 117).
436
+ - `cd web/admin && bun run test` → 21 pass / 0 fail.
437
+ - `bun run typecheck` → clean (root + web/admin).
438
+ - `bunx biome check .` → clean.
439
+ - `cd web/admin && bun run build` → `dist/admin/` populated.
440
+ - `bin/parachute-app.ts --version` → 0.1.0-rc.3.
441
+ - `bin/parachute-app.ts --help` → shows full Phase 1.2 verb list.
442
+
443
+ ## [0.1.0-rc.2] - 2026-05-22
444
+
445
+ feat(app): Phase 1.1 — core UI hosting with smart cache headers + PWA opt-in.
446
+
447
+ Replaces the Phase 1.0 stub with a real `serve` daemon. App now scans
448
+ `$PARACHUTE_HOME/app/uis/` for declared UIs, validates each meta.json,
449
+ mounts each bundle at its declared path under `/app/`, and serves the
450
+ dist/ contents with smart cache headers + SPA-routing fallback.
451
+
452
+ ### Added
453
+
454
+ - `src/config.ts` — load + validate `$PARACHUTE_HOME/app/config.json`,
455
+ with sensible defaults. Missing file is OK; malformed file fails fast.
456
+ Honors `PARACHUTE_HOME` env var.
457
+ - `src/meta-schema.ts` — hand-rolled validator for per-UI meta.json.
458
+ Required fields: `name` (pattern `^[a-z][a-z0-9-]*$`), `displayName`,
459
+ `path` (pattern `^/app/[a-z0-9-]+$`). Optional: `tagline`, `version`,
460
+ `iconUrl`, `scopes_required` (defaults to `["vault:*:read"]`),
461
+ `vault_default`, `pwa` (default false), `pwa_service_worker` (required
462
+ when `pwa: true`), `public` (default false). Exposes
463
+ `InvalidMetaError` with a flat `details` list.
464
+ - `src/ui-registry.ts` — `scanUis()` scans the uis-dir, validates each
465
+ meta.json + dist/index.html, resolves mount-path collisions
466
+ deterministically (alphabetical-by-name wins, losers demoted to
467
+ `status: "collision"`). Returns `{registered, skipped}`. The reserved
468
+ path `/app/admin` is rejected for hosted UIs (admin SPA lands in
469
+ Phase 1.2).
470
+ - `src/cache-headers.ts` — `cacheHeadersFor(filename, meta?)` returns
471
+ smart `Cache-Control` headers per design doc section 18:
472
+ index.html → `no-cache, no-store, must-revalidate`; content-hashed
473
+ assets (matching `[a-f0-9]{8,}`) → `public, max-age=31536000,
474
+ immutable`; non-hashed assets → `public, max-age=3600`; PWA service
475
+ worker (when meta opts in) → `no-cache`.
476
+ - `src/http-server.ts` — Bun.serve loopback HTTP server on port 1946.
477
+ Routes: `GET /healthz` + `GET /app/healthz` (open, returns UI counts),
478
+ `GET /.parachute/info` + `/.parachute/config/schema` + `/.parachute/config`
479
+ (open; no secrets in app config), and per-UI bundle serving with SPA
480
+ fallback. Path-traversal-safe. HEAD requests supported. 405 on
481
+ non-GET methods (Phase 1.2 opens up POST/PUT/DELETE for admin
482
+ endpoints).
483
+ - `src/services-manifest.ts` + `src/self-register.ts` — mirrors runner's
484
+ pattern exactly. Self-registers app's row into
485
+ `~/.parachute/services.json` on `serve` boot. Best-effort: write
486
+ failures are logged + swallowed. Existing operator-set ports are
487
+ preserved across restarts. `extraFields` hook lets Phase 1.2 stamp the
488
+ per-UI `uis` map without changing the signature.
489
+ - `src/__tests__/{config,meta-schema,ui-registry,cache-headers,http-server,self-register,serve,cli}.test.ts` —
490
+ unit + integration coverage. 114 tests total (was 1).
491
+
492
+ ### Changed
493
+
494
+ - `bin/parachute-app.ts` — `serve` verb now boots the real daemon (no
495
+ more stub). Wires SIGINT/SIGTERM to graceful shutdown. Help text
496
+ updated to reflect Phase 1.1 capabilities.
497
+ - `src/index.ts` — `serve()` and `runOnce()` implemented; the per-verb
498
+ stubs for Phase 1.2 (`addUi`, `removeUi`, `listUis`, `reloadUi`) and
499
+ Phase 1.3 (`setDevMode`) remain as documented placeholders. Re-exports
500
+ the new modules.
501
+ - `.parachute/info` — version bumped to 0.1.0-rc.2.
502
+
503
+ ### Verified
504
+
505
+ - `bun test` → 114 pass / 0 fail.
506
+ - `bun run typecheck` → clean.
507
+ - `bunx biome check .` → clean.
508
+ - Live smoke against `~/.parachute/app/uis/test-ui/`:
509
+ - `/app/healthz` returns `{status:"ok",uis:1,skipped:0}`.
510
+ - `/app/test-ui/` returns the index.html with
511
+ `Cache-Control: no-cache, no-store, must-revalidate`.
512
+ - `/app/test-ui/app.abc12345.js` returns `Cache-Control: public,
513
+ max-age=31536000, immutable`.
514
+ - `/app/test-ui/style.css` returns `Cache-Control: public,
515
+ max-age=3600`.
516
+ - `/app/test-ui/some/spa/route` falls through to index.html.
517
+ - `~/.parachute/services.json` has the parachute-app row with
518
+ `port: 19460`, `paths: ["/app","/.parachute"]`, `installDir`.
519
+
520
+ ## [0.1.0-rc.1] - 2026-05-21
521
+
522
+ Initial scaffold per design doc. Module-protocol-compliant skeleton with stub bin and library entry — no UI hosting, no admin endpoints, no OAuth DCR yet. Those land in Phase 1.1+.
523
+
524
+ ### Added
525
+
526
+ - `.parachute/module.json` — manifest declaring `port: 1946`, paths `["/app", "/.parachute"]`, health `/app/healthz`, scopes `app:read` + `app:admin`. No `kind` field (per hub#301 migration — `kind` is being dropped from the manifest validator).
527
+ - `.parachute/info` — module identity (name, displayName, tagline, version, capabilities).
528
+ - `.parachute/config/schema` — Draft-07 JSON Schema for `$PARACHUTE_HOME/app/config.json`: `hub_url`, `auto_register_oauth_clients`, `disabled`, `default_scope_required`, `dev_mode_allowed`.
529
+ - `bin/parachute-app.ts` — CLI with `--help` listing planned verbs by phase, `--version` printing from package.json, every subcommand stubbed to a phase-tagged not-yet-implemented message.
530
+ - `src/index.ts` — library surface: `VERSION`, `DEFAULT_PORT`, `DEFAULT_MOUNT`, plus stub functions (`serve`, `runOnce`, `addUi`, `removeUi`, `listUis`, `reloadUi`, `setDevMode`) each throwing a phase-tagged Error.
531
+ - `src/__tests__/scaffold.test.ts` — sanity test asserting `VERSION` matches `package.json#version`.
532
+ - `package.json` — `@openparachute/app@0.1.0-rc.1`, `bin: parachute-app → ./bin/parachute-app.ts`, scripts for `start` / `test` / `typecheck` / `lint`.
533
+ - `tsconfig.json`, `biome.json`, `.gitignore`, `LICENSE` (AGPL-3.0), `README.md` — standard repo scaffolding mirroring parachute-runner.
534
+
535
+ ### Design
536
+
537
+ - [`2026-05-21-parachute-apps-design.md`](https://github.com/ParachuteComputer/parachute.computer/blob/main/design/2026-05-21-parachute-apps-design.md)