@openparachute/app 0.2.0-rc.4

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,405 @@
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.1] + [app-client 0.1.0-rc.1] - 2026-05-21
13
+
14
+ feat(app): Phase 2.0 — extract `@openparachute/app-client` shared
15
+ library as a sub-package + add `required_schema` to meta.json
16
+ (folds [patterns#57](https://github.com/ParachuteComputer/parachute-patterns/issues/57)).
17
+
18
+ This is the monorepo-restructure release. The repo grows a workspace
19
+ shape with two publishable packages and a workspace-only admin SPA.
20
+ Each hosted app today re-implements OAuth + vault REST + token storage
21
+ from scratch (Notes did this; the Gitcoin Brain UI has its own); the
22
+ new `@openparachute/app-client` package extracts the canonical pattern.
23
+
24
+ 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).
25
+
26
+ ### Monorepo restructure
27
+
28
+ - `packages/app-host/` — the host module (formerly the entire repo).
29
+ Bumped to `0.2.0-rc.1` (minor for the restructure).
30
+ - `packages/app-client/` — NEW shared library at `0.1.0-rc.1`.
31
+ - Root `package.json` becomes the workspace root (private
32
+ `@openparachute/app-monorepo`). Workspaces: `packages/*` + `web/admin`.
33
+ - `web/admin/` (admin SPA) unchanged in shape; build output redirected
34
+ to `packages/app-host/dist/admin/` so the daemon's `defaultAdminDir`
35
+ still resolves correctly. Bumped to `0.2.0-rc.1` to mirror the host.
36
+
37
+ ### `@openparachute/app-client` 0.1.0-rc.1 — public surface
38
+
39
+ Tree-shake-friendly subpath exports + a barrel:
40
+
41
+ | Subpath | Surface |
42
+ |---|---|
43
+ | `oauth` | `ParachuteOAuth` driver class (PKCE + same-hub auto-trust); `PendingApprovalError`, `RefreshHttpError`, `InsecureContextError` |
44
+ | `vault-client` | `VaultClient` REST client with auto-refresh on 401/403; `VaultAuthError` (carries `errorType` per notes#150), `VaultNotFoundError`, `VaultUnreachableError`, `VaultConflictError`, `VaultTargetExistsError`, `VaultUploadError` |
45
+ | `token-storage` | `loadToken` / `saveToken` / `clearToken` / `clearAllTokensForApp`; key format `parachute_token:<app-name>:<vault-scope>`; auto-prunes expired tokens that have no refresh_token |
46
+ | `sw-reload` | `reloadAfterServiceWorkerUpdate` (lifted from notes#148) |
47
+ | `vault-id` | `vaultIdFromUrl` + `normalizeVaultUrl` (notes#149 URL-drift fix) |
48
+
49
+ Notes-canonical implementation extracted with the following deltas:
50
+ - All paths handle the no-`window` / SSR case (token-storage falls back
51
+ to a `NULL_STORAGE` shim; sessionStorage in `ParachuteOAuth` likewise).
52
+ - Cursor pagination (`queryNotesCursor`) reads `X-Next-Cursor` and
53
+ preserves the cursor through the auth-retry path.
54
+ - `ParachuteOAuth.beginFlow` accepts a `vaultName` opt that adds the
55
+ `vault=<name>` hint to `/oauth/authorize` for the multi-vault
56
+ narrow-on-pick pattern Notes uses today.
57
+
58
+ ### `@openparachute/app` 0.2.0-rc.1 — meta.json `required_schema`
59
+
60
+ Per patterns#57 ("Surfaces declare required vault schema"), `meta.json`
61
+ gains an optional `required_schema` field:
62
+
63
+ ```json
64
+ {
65
+ "required_schema": {
66
+ "tags": [
67
+ {
68
+ "name": "capture",
69
+ "description": "Quick captures",
70
+ "fields": {
71
+ "source": { "type": "string", "required": true },
72
+ "createdAt": { "type": "date" }
73
+ }
74
+ }
75
+ ]
76
+ }
77
+ }
78
+ ```
79
+
80
+ Phase 2.0 scope: **validate + surface in admin SPA**. Phase 2.1+ will
81
+ auto-provision missing tag-identity rows in vault via
82
+ `VaultClient.updateTag` at install time; that's tracked separately.
83
+
84
+ The admin SPA's modules table grows a per-row "Schema requirements"
85
+ expandable summary; the per-UI info page renders the full declaration.
86
+
87
+ ### Verified
88
+
89
+ | Suite | Before | After |
90
+ |---|---|---|
91
+ | `bun test packages/app-host/src/` | 270 / 0 | 281 / 0 |
92
+ | `bun test packages/app-client/src/` | n/a | 80 / 0 |
93
+ | `cd web/admin && bun run test` | 31 / 0 | 40 / 0 |
94
+
95
+ Typecheck clean (`tsc --noEmit` across all three). Build clean
96
+ (`bun run build` from root builds app-client then app-host).
97
+
98
+ ---
99
+
100
+ ## [0.1.0-rc.4] - 2026-05-22
101
+
102
+ feat(app): Phase 1.3 — dev mode with SSE live-reload (closes Phase 1).
103
+
104
+ Phase 1.3 closes Phase 1 of parachute-app and resolves the recurring
105
+ "edit code, build, browser shows old" frustration tracked in
106
+ [parachute-notes#151](https://github.com/ParachuteComputer/parachute-notes/issues/151)
107
+ at the platform level. Adds operator-triggered dev mode: `parachute-app
108
+ dev <name>` flips a UI into a no-cache mode + injects an EventSource
109
+ shim into `index.html` that reloads the tab when the operator runs
110
+ `parachute-app dev <name> --trigger` after a rebuild. Reference:
111
+ [design doc section 18](https://github.com/ParachuteComputer/parachute.computer/blob/main/design/2026-05-21-parachute-apps-design.md#18-caching--reload-strategy).
112
+
113
+ ### Added
114
+
115
+ - `src/dev-mode.ts` — process-local, in-memory dev-mode state. One Map
116
+ for `name → { enabled, enabledAt, watchDir?, buildCmd? }`, one Map
117
+ for `name → Set<DevReloadSubscriber>`. Exports `enableDevMode`,
118
+ `disableDevMode`, `isDevMode`, `listDevMode`, `getDevMode`,
119
+ `addSubscriber`, `removeSubscriber`, `broadcastReload`,
120
+ `subscriberCount`, `closeAllSubscribers`, `resetDevMode`. Idempotent
121
+ enable preserves `enabledAt`; disable closes every connected SSE
122
+ stream so the next request resumes production cache headers cleanly.
123
+ - `src/dev-injection.ts` — HTML script-injection (string scan, no
124
+ cheerio dep). Inserts `<script id="parachute-app-dev-reload">` just
125
+ before `</head>`, with fallbacks (`before-script` → `after-body` →
126
+ `append`) for unusual document structures. Idempotent via the marker
127
+ id — re-rendering the same document doesn't duplicate the tag. The
128
+ script body opens an EventSource against `/app/<name>/_dev/reload`
129
+ and `window.location.reload()`s on `reload` events (200ms debounce).
130
+ - `src/dev-routes.ts` — Phase 1.3 HTTP endpoints:
131
+ - `GET /app/<name>/_dev/reload` (UNAUTHENTICATED) — SSE stream;
132
+ 404 when the UI isn't in dev mode. Emits a `: connected` keepalive
133
+ on accept; broadcasts `event: reload\ndata: {"timestamp": ...}` on
134
+ trigger. Disconnects clean up via the stream's `cancel` hook.
135
+ - `POST /app/<name>/dev/enable` (`app:admin`) — flip on. Honors
136
+ `config.dev_mode_allowed: false` with 409.
137
+ - `POST /app/<name>/dev/disable` (`app:admin`) — flip off + close
138
+ every subscriber.
139
+ - `POST /app/<name>/dev/trigger` (`app:admin`) — broadcast `reload`;
140
+ 409 when dev mode is off. Returns `{ notified: <count> }`.
141
+ - `GET /app/<name>/dev` (`app:read`) — per-UI status.
142
+ - `GET /app/dev/list` (`app:read`) — UIs currently in dev mode.
143
+ - `src/cache-headers.ts` — `cacheHeadersFor` takes a `devMode` boolean.
144
+ When true, every response is `no-cache, no-store, must-revalidate` —
145
+ overrides immutable on hashed assets AND `no-cache` on the PWA SW.
146
+ - `src/http-server.ts` — wires dev-routes ahead of admin routes; per-
147
+ request `isDevMode(meta.name)` check feeds both the cache headers
148
+ and the index.html injection. `serveFileWithHeaders` accepts a
149
+ `devMode` parameter; when true + filename is `index.html`, it parses
150
+ the body via `injectDevReloadScript` before responding. HEAD reports
151
+ the injected byte length.
152
+ - `src/index.ts` — re-exports the dev-mode + dev-injection surface,
153
+ exposes `routeDev` + `DevRoutesOpts`, replaces the Phase 1.3 stub
154
+ `setDevMode` with a real wrapper.
155
+ - `bin/parachute-app.ts` — replaces the Phase 1.3 stub with four
156
+ sub-verbs:
157
+ - `parachute-app dev <name>` — enable (idempotent)
158
+ - `parachute-app dev <name> --off` — disable
159
+ - `parachute-app dev <name> --trigger` — broadcast reload
160
+ - `parachute-app dev list` — show UIs currently in dev mode
161
+ Help text reflects the full Phase 1.3 verb set.
162
+ - `web/admin/src/lib/api.ts` — typed helpers: `enableDevMode`,
163
+ `disableDevMode`, `triggerReload`, `getDevModeStatus`, `listDevMode`.
164
+ - `web/admin/src/routes/Modules.tsx` — per-row "Dev" badge + "Enable
165
+ dev" / "Disable dev" / "Trigger reload" buttons. Refreshes the
166
+ dev-status map alongside the UI list.
167
+ - Tests:
168
+ - `src/__tests__/dev-mode.test.ts` (15 tests) — state, subscribers,
169
+ broadcast reaping.
170
+ - `src/__tests__/dev-injection.test.ts` (10 tests) — happy path +
171
+ idempotence + all three fallback branches + escape defense.
172
+ - `src/__tests__/dev-routes.test.ts` (14 tests) — every endpoint +
173
+ auth gates + SSE subscribe / broadcast / cancel.
174
+ - `src/__tests__/dev-integration.test.ts` (10 tests) — full
175
+ end-to-end via Bun.serve including script injection, no-cache
176
+ override, SSE broadcast, dev-list, HEAD content-length.
177
+ - `src/__tests__/cache-headers.test.ts` — extra coverage for the
178
+ `devMode` parameter.
179
+ - `src/__tests__/cli.test.ts` — refreshed for the new `dev` verbs.
180
+ - `web/admin/src/routes/Modules.test.tsx` — updated to mock the
181
+ `/app/dev/list` companion fetch + assert the new dev controls.
182
+
183
+ ### Changed
184
+
185
+ - Bumped to `0.1.0-rc.4`. `.parachute/info` capabilities now include
186
+ `dev-mode-sse`.
187
+ - HTTP server routing: dev-routes dispatcher fires ahead of admin-routes
188
+ so the per-UI `_dev/reload` path doesn't race with the admin matcher.
189
+ - `cacheHeadersFor` signature gains a third `devMode = false` parameter
190
+ (backwards-compatible — existing meta-less callers continue to work).
191
+ - Admin SPA's Modules table grew a "Dev" column; existing layout
192
+ preserved.
193
+
194
+ ### Verified
195
+
196
+ - `bun test src/` → 270 pass / 0 fail (was 213).
197
+ - `cd web/admin && bun run test` → 31 pass / 0 fail (was 21).
198
+ - `bun run typecheck` → clean (root + web/admin).
199
+ - `bunx biome check .` → clean.
200
+ - `bun run build` → `dist/admin/` populated.
201
+ - `bin/parachute-app.ts --version` → 0.1.0-rc.4.
202
+ - `bin/parachute-app.ts --help` → shows the four `dev` sub-verbs.
203
+
204
+ ## [0.1.0-rc.3] - 2026-05-21
205
+
206
+ feat(app): Phase 1.2 — admin endpoints + DCR + npm-fetch + Vite+React admin SPA.
207
+
208
+ Phase 1.2 takes the bundled-UI-host daemon from "operator manually drops
209
+ dist/ into uis/" to "operator runs `parachute-app add <source>` and the
210
+ daemon handles copy + DCR + re-scan." Adds the admin HTTP surface, the
211
+ Dynamic Client Registration call to hub, an npm-fetch shorthand for
212
+ sourcing UIs by package specifier, and a Vite + React admin SPA mounted
213
+ at `/app/admin/`.
214
+
215
+ ### Added
216
+
217
+ - `src/auth.ts` — hub-JWT validation via `@openparachute/scope-guard@^0.3.0`.
218
+ Audience `app`; scopes `app:read` (list/info) and `app:admin` (add/remove/
219
+ reload). `enforceScope` mirrors runner's pattern; `hasReadAccess` lets
220
+ admin imply read.
221
+ - `src/operator-token.ts` — operator bearer sourcing for outbound DCR
222
+ calls. Priority: `PARACHUTE_HUB_TOKEN` env > `~/.parachute/operator.token`
223
+ file (chmod 0o600 required on Unix). Missing token returns undefined; the
224
+ caller decides whether that's fatal.
225
+ - `src/dcr.ts` — RFC 7591 Dynamic Client Registration with hub. Sends
226
+ `client_name`, `redirect_uris` (`/app/<name>/` + `/app/<name>/oauth-callback`),
227
+ `scope` (joined), `token_endpoint_auth_method: "none"`, `grant_types:
228
+ ["authorization_code"]`, `response_types: ["code"]`. Persists the returned
229
+ `client_id` to `~/.parachute/app/uis/<name>/.oauth-client.json` (chmod 0o600).
230
+ Surfaces hub errors as a typed `DcrError` (status: hub_unreachable /
231
+ hub_rejected / invalid_response). Best-effort `DELETE /oauth/clients/<id>`
232
+ on remove; tolerates 404/405 (RFC 7592 not universally implemented yet).
233
+ - `src/npm-fetch.ts` — `bun add <spec>` into a `/tmp/parachute-app-staging-*`
234
+ dir, then copies `node_modules/<pkg>/dist/` into the UI's home. Distinguishes
235
+ 404 / network / generic errors by sniffing stderr. Cleanup always runs.
236
+ Supports plain names, scoped names, and `@version` tails.
237
+ - `src/admin-routes.ts` — the Phase 1.2 admin endpoints:
238
+ - `GET /app/list` (`app:read`) — serialized UI summaries + skipped list
239
+ - `POST /app/add` (`app:admin`) — accepts local path OR npm spec; copies
240
+ bundle + writes meta.json + (optionally) fires DCR + re-scans
241
+ - `DELETE /app/<name>` (`app:admin`) — revokes OAuth + removes dir +
242
+ re-scans
243
+ - `POST /app/<name>/reload` (`app:admin`) — re-scans without daemon restart
244
+ - `GET /app/<name>/info` (`app:read`) — full info: meta + oauth + paths
245
+ - `GET /app/<name>/oauth-client` — UNAUTHENTICATED — returns
246
+ `{client_id, hub_url, scope, redirect_uris}` for the UI to use at boot
247
+ - Auto-rejects `/app/admin` as a reserved mount path.
248
+ - Validates name + path patterns; rejects collisions with 409.
249
+ - After every mutation: re-runs `scanUis()` + refreshes `services.json`
250
+ with the per-UI `uis` map (design doc section 12 shape).
251
+ - `src/http-server.ts` — wires the admin routes into the existing Bun.serve
252
+ handler. POST/DELETE now flow through the admin matcher; non-admin POST/
253
+ DELETE returns 404 (was 405). Unknown methods still return 405. New
254
+ `/app/admin/[*]` static mount serves the built SPA from `dist/admin/`;
255
+ falls back to a dev-time placeholder when the bundle is absent.
256
+ - `web/admin/` — Vite + React + TypeScript admin SPA. React 19, react-router
257
+ 7. Routes: `/` (Modules), `/add` (Add UI form), `/info/:name`. Auth via
258
+ `localStorage["parachute_operator_token"]` (Phase 1.3 wires hub-session
259
+ auth). Builds to root `dist/admin/`. Per-UI Reload + Remove buttons hit
260
+ the live admin endpoints. Skipped UIs surface inline with their failure
261
+ reason.
262
+ - `bin/parachute-app.ts` — `add`, `remove`, `list`, `reload` verbs are no
263
+ longer stubs. Each calls the local daemon's admin endpoints over HTTP
264
+ (`PARACHUTE_APP_URL` env overrides). Sources the operator bearer via the
265
+ same `readOperatorToken` the daemon uses.
266
+ - Tests:
267
+ - `src/__tests__/auth.test.ts` — bearer extraction, scope checks,
268
+ `validateBearer` 401 paths, `getHubOrigin` resolution
269
+ - `src/__tests__/operator-token.test.ts` — env vs file priority, mode
270
+ 0o600 defense
271
+ - `src/__tests__/dcr.test.ts` — DCR request shape, operator-bearer
272
+ forwarding, hub-error surfacing, file persistence + revocation
273
+ - `src/__tests__/npm-fetch.test.ts` — spec parsing, fake-bun-add
274
+ integration, error-code mapping
275
+ - `src/__tests__/admin-routes.test.ts` — auth gates + full happy paths
276
+ with the `enforceScopeFn` test seam
277
+ - `src/__tests__/admin-integration.test.ts` — end-to-end add/delete/
278
+ reload through Bun.serve
279
+ - `web/admin/src/lib/api.test.ts` — api.ts wrapper coverage
280
+ - `web/admin/src/routes/Modules.test.tsx` — list view, error banner,
281
+ Reload + Remove button flows
282
+ - `web/admin/src/routes/Add.test.tsx` — form submission shape + success
283
+ rendering
284
+ - `web/admin/src/App.test.tsx` — shell + token banner
285
+
286
+ ### Changed
287
+
288
+ - Bumped to `0.1.0-rc.3`. `.parachute/info` capabilities now include
289
+ `admin-spa`.
290
+ - `bin/parachute-app.ts` help text reflects the live `add`/`remove`/`list`/
291
+ `reload` verbs.
292
+ - `src/http-server.ts` 405 policy: POST/DELETE no longer return 405 globally;
293
+ they flow to admin routes and fall through to 404 when no admin route
294
+ matches. PATCH and other unhandled methods still return 405.
295
+ - `package.json#files` now includes `dist/admin/**` so the npm-published
296
+ bundle ships the admin SPA. Added `build` / `test:admin` / `typecheck:all`
297
+ scripts coordinating root + web/admin.
298
+ - `package.json` now depends on `@openparachute/scope-guard@^0.3.0` for
299
+ hub-JWT validation.
300
+
301
+ ### Verified
302
+
303
+ - `bun test ./src` → 213 pass / 0 fail (was 117).
304
+ - `cd web/admin && bun run test` → 21 pass / 0 fail.
305
+ - `bun run typecheck` → clean (root + web/admin).
306
+ - `bunx biome check .` → clean.
307
+ - `cd web/admin && bun run build` → `dist/admin/` populated.
308
+ - `bin/parachute-app.ts --version` → 0.1.0-rc.3.
309
+ - `bin/parachute-app.ts --help` → shows full Phase 1.2 verb list.
310
+
311
+ ## [0.1.0-rc.2] - 2026-05-22
312
+
313
+ feat(app): Phase 1.1 — core UI hosting with smart cache headers + PWA opt-in.
314
+
315
+ Replaces the Phase 1.0 stub with a real `serve` daemon. App now scans
316
+ `$PARACHUTE_HOME/app/uis/` for declared UIs, validates each meta.json,
317
+ mounts each bundle at its declared path under `/app/`, and serves the
318
+ dist/ contents with smart cache headers + SPA-routing fallback.
319
+
320
+ ### Added
321
+
322
+ - `src/config.ts` — load + validate `$PARACHUTE_HOME/app/config.json`,
323
+ with sensible defaults. Missing file is OK; malformed file fails fast.
324
+ Honors `PARACHUTE_HOME` env var.
325
+ - `src/meta-schema.ts` — hand-rolled validator for per-UI meta.json.
326
+ Required fields: `name` (pattern `^[a-z][a-z0-9-]*$`), `displayName`,
327
+ `path` (pattern `^/app/[a-z0-9-]+$`). Optional: `tagline`, `version`,
328
+ `iconUrl`, `scopes_required` (defaults to `["vault:*:read"]`),
329
+ `vault_default`, `pwa` (default false), `pwa_service_worker` (required
330
+ when `pwa: true`), `public` (default false). Exposes
331
+ `InvalidMetaError` with a flat `details` list.
332
+ - `src/ui-registry.ts` — `scanUis()` scans the uis-dir, validates each
333
+ meta.json + dist/index.html, resolves mount-path collisions
334
+ deterministically (alphabetical-by-name wins, losers demoted to
335
+ `status: "collision"`). Returns `{registered, skipped}`. The reserved
336
+ path `/app/admin` is rejected for hosted UIs (admin SPA lands in
337
+ Phase 1.2).
338
+ - `src/cache-headers.ts` — `cacheHeadersFor(filename, meta?)` returns
339
+ smart `Cache-Control` headers per design doc section 18:
340
+ index.html → `no-cache, no-store, must-revalidate`; content-hashed
341
+ assets (matching `[a-f0-9]{8,}`) → `public, max-age=31536000,
342
+ immutable`; non-hashed assets → `public, max-age=3600`; PWA service
343
+ worker (when meta opts in) → `no-cache`.
344
+ - `src/http-server.ts` — Bun.serve loopback HTTP server on port 1946.
345
+ Routes: `GET /healthz` + `GET /app/healthz` (open, returns UI counts),
346
+ `GET /.parachute/info` + `/.parachute/config/schema` + `/.parachute/config`
347
+ (open; no secrets in app config), and per-UI bundle serving with SPA
348
+ fallback. Path-traversal-safe. HEAD requests supported. 405 on
349
+ non-GET methods (Phase 1.2 opens up POST/PUT/DELETE for admin
350
+ endpoints).
351
+ - `src/services-manifest.ts` + `src/self-register.ts` — mirrors runner's
352
+ pattern exactly. Self-registers app's row into
353
+ `~/.parachute/services.json` on `serve` boot. Best-effort: write
354
+ failures are logged + swallowed. Existing operator-set ports are
355
+ preserved across restarts. `extraFields` hook lets Phase 1.2 stamp the
356
+ per-UI `uis` map without changing the signature.
357
+ - `src/__tests__/{config,meta-schema,ui-registry,cache-headers,http-server,self-register,serve,cli}.test.ts` —
358
+ unit + integration coverage. 114 tests total (was 1).
359
+
360
+ ### Changed
361
+
362
+ - `bin/parachute-app.ts` — `serve` verb now boots the real daemon (no
363
+ more stub). Wires SIGINT/SIGTERM to graceful shutdown. Help text
364
+ updated to reflect Phase 1.1 capabilities.
365
+ - `src/index.ts` — `serve()` and `runOnce()` implemented; the per-verb
366
+ stubs for Phase 1.2 (`addUi`, `removeUi`, `listUis`, `reloadUi`) and
367
+ Phase 1.3 (`setDevMode`) remain as documented placeholders. Re-exports
368
+ the new modules.
369
+ - `.parachute/info` — version bumped to 0.1.0-rc.2.
370
+
371
+ ### Verified
372
+
373
+ - `bun test` → 114 pass / 0 fail.
374
+ - `bun run typecheck` → clean.
375
+ - `bunx biome check .` → clean.
376
+ - Live smoke against `~/.parachute/app/uis/test-ui/`:
377
+ - `/app/healthz` returns `{status:"ok",uis:1,skipped:0}`.
378
+ - `/app/test-ui/` returns the index.html with
379
+ `Cache-Control: no-cache, no-store, must-revalidate`.
380
+ - `/app/test-ui/app.abc12345.js` returns `Cache-Control: public,
381
+ max-age=31536000, immutable`.
382
+ - `/app/test-ui/style.css` returns `Cache-Control: public,
383
+ max-age=3600`.
384
+ - `/app/test-ui/some/spa/route` falls through to index.html.
385
+ - `~/.parachute/services.json` has the parachute-app row with
386
+ `port: 19460`, `paths: ["/app","/.parachute"]`, `installDir`.
387
+
388
+ ## [0.1.0-rc.1] - 2026-05-21
389
+
390
+ 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+.
391
+
392
+ ### Added
393
+
394
+ - `.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).
395
+ - `.parachute/info` — module identity (name, displayName, tagline, version, capabilities).
396
+ - `.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`.
397
+ - `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.
398
+ - `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.
399
+ - `src/__tests__/scaffold.test.ts` — sanity test asserting `VERSION` matches `package.json#version`.
400
+ - `package.json` — `@openparachute/app@0.1.0-rc.1`, `bin: parachute-app → ./bin/parachute-app.ts`, scripts for `start` / `test` / `typecheck` / `lint`.
401
+ - `tsconfig.json`, `biome.json`, `.gitignore`, `LICENSE` (AGPL-3.0), `README.md` — standard repo scaffolding mirroring parachute-runner.
402
+
403
+ ### Design
404
+
405
+ - [`2026-05-21-parachute-apps-design.md`](https://github.com/ParachuteComputer/parachute.computer/blob/main/design/2026-05-21-parachute-apps-design.md)