@openparachute/hub 0.5.13 → 0.5.14-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/__tests__/account-home-ui.test.ts +163 -0
- package/src/__tests__/admin-handlers.test.ts +74 -0
- package/src/__tests__/admin-host-admin-token.test.ts +62 -0
- package/src/__tests__/admin-vault-admin-token.test.ts +44 -0
- package/src/__tests__/api-account.test.ts +191 -1
- package/src/__tests__/api-modules-ops.test.ts +97 -0
- package/src/__tests__/api-modules.test.ts +32 -32
- package/src/__tests__/api-users.test.ts +383 -11
- package/src/__tests__/chrome-strip.test.ts +15 -15
- package/src/__tests__/hub-db.test.ts +194 -29
- package/src/__tests__/hub-server.test.ts +23 -23
- package/src/__tests__/notes-redirect.test.ts +20 -20
- package/src/__tests__/oauth-handlers.test.ts +722 -28
- package/src/__tests__/serve.test.ts +9 -9
- package/src/__tests__/services-manifest.test.ts +40 -40
- package/src/__tests__/setup-wizard.test.ts +493 -25
- package/src/__tests__/setup.test.ts +1 -1
- package/src/__tests__/status.test.ts +39 -0
- package/src/__tests__/users.test.ts +396 -9
- package/src/__tests__/well-known.test.ts +9 -9
- package/src/account-home-ui.ts +434 -0
- package/src/admin-handlers.ts +49 -17
- package/src/admin-host-admin-token.ts +25 -0
- package/src/admin-vault-admin-token.ts +17 -0
- package/src/api-account.ts +72 -6
- package/src/api-modules-ops.ts +52 -16
- package/src/api-modules.ts +3 -3
- package/src/api-users.ts +468 -55
- package/src/bun-link.ts +55 -0
- package/src/chrome-strip.ts +6 -6
- package/src/commands/install.ts +8 -21
- package/src/commands/status.ts +10 -1
- package/src/help.ts +2 -2
- package/src/hub-db.ts +42 -0
- package/src/hub-server.ts +69 -10
- package/src/hub-settings.ts +2 -2
- package/src/hub.ts +6 -6
- package/src/notes-redirect.ts +5 -5
- package/src/oauth-handlers.ts +278 -173
- package/src/oauth-ui.ts +18 -2
- package/src/service-spec.ts +39 -18
- package/src/setup-wizard.ts +489 -42
- package/src/users.ts +307 -29
- package/web/ui/dist/assets/index-tRmPbbC7.js +61 -0
- package/web/ui/dist/index.html +1 -1
- package/web/ui/dist/assets/index-Dzrbe6EP.js +0 -61
package/src/chrome-strip.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* (above that threshold the response is almost certainly not an HTML shell
|
|
14
14
|
* anyway — SPA index.html files are < 16 KB in this ecosystem).
|
|
15
15
|
*
|
|
16
|
-
* Opt-out: hub-side path-prefix deny list. The Notes PWA at `/
|
|
16
|
+
* Opt-out: hub-side path-prefix deny list. The Notes PWA at `/surface/notes/*`
|
|
17
17
|
* is the canonical opt-out — it owns its own chrome (see design-system §7
|
|
18
18
|
* "Where NOT to inject" + AUDIT §4: "Notes is the proof this can work: own
|
|
19
19
|
* application, looks distinctively Notes, reads as Parachute because the
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Why path-based and not module-declared:
|
|
23
23
|
* - Notes is a `uis[]` sub-unit of parachute-app, not its own module —
|
|
24
24
|
* adding `chrome: "off"` to parachute-app's module.json would suppress
|
|
25
|
-
* chrome on `/
|
|
25
|
+
* chrome on `/surface/admin/*` too (wrong: that surface SHOULD get chrome).
|
|
26
26
|
* - The per-uis well-known fan-out (workstream C/4) is in flight but the
|
|
27
27
|
* hub side doesn't yet thread per-uis metadata into proxy dispatch.
|
|
28
28
|
* - HTML meta-tag peeking adds parsing overhead on every response.
|
|
@@ -46,10 +46,10 @@ import { CSRF_FIELD_NAME, ensureCsrfToken } from "./csrf.ts";
|
|
|
46
46
|
* prefix" or "pathname startsWith prefix" — the same shape as
|
|
47
47
|
* `findServiceUpstream`'s mount comparison.
|
|
48
48
|
*
|
|
49
|
-
* `/
|
|
49
|
+
* `/surface/notes/` covers the Notes PWA bundled by parachute-app. Notes is a
|
|
50
50
|
* destination, not chrome; it owns its own header (see design-system.md §7).
|
|
51
51
|
*/
|
|
52
|
-
export const CHROME_OPT_OUT_PREFIXES: readonly string[] = ["/
|
|
52
|
+
export const CHROME_OPT_OUT_PREFIXES: readonly string[] = ["/surface/notes/"];
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Buffer size cap. Responses larger than this are passed through unchanged.
|
|
@@ -208,8 +208,8 @@ function renderSignedOutCluster(nextPath: string): string {
|
|
|
208
208
|
* when any opt-out prefix matches (`pathname === prefix` or
|
|
209
209
|
* `pathname startsWith prefix`).
|
|
210
210
|
*
|
|
211
|
-
* Match shape mirrors `findServiceUpstream` so an opt-out for `"/
|
|
212
|
-
* suppresses chrome for `/
|
|
211
|
+
* Match shape mirrors `findServiceUpstream` so an opt-out for `"/surface/notes/"`
|
|
212
|
+
* suppresses chrome for `/surface/notes`, `/surface/notes/`, and every sub-path.
|
|
213
213
|
*/
|
|
214
214
|
export function shouldInjectChrome(
|
|
215
215
|
pathname: string,
|
package/src/commands/install.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { createConnection } from "node:net";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
5
4
|
import { autoWireScribeAuth } from "../auto-wire.ts";
|
|
5
|
+
import { bunGlobalPrefixes, isLinked as defaultIsLinkedShared } from "../bun-link.ts";
|
|
6
6
|
import { CONFIG_DIR, SERVICES_MANIFEST_PATH } from "../config.ts";
|
|
7
7
|
import {
|
|
8
8
|
type ModuleManifest,
|
|
@@ -260,25 +260,12 @@ async function defaultRunner(cmd: readonly string[]): Promise<number> {
|
|
|
260
260
|
return await proc.exited;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function defaultIsLinked(pkg: string): boolean {
|
|
272
|
-
for (const prefix of bunGlobalPrefixes()) {
|
|
273
|
-
const path = join(prefix, ...pkg.split("/"));
|
|
274
|
-
try {
|
|
275
|
-
if (lstatSync(path).isSymbolicLink()) return true;
|
|
276
|
-
} catch {
|
|
277
|
-
// Not present at this prefix; try the next.
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return false;
|
|
281
|
-
}
|
|
263
|
+
// `bunGlobalPrefixes` + `defaultIsLinked` were extracted to `src/bun-link.ts`
|
|
264
|
+
// so the wizard's parallel install path (`api-modules-ops.ts:runInstall`) can
|
|
265
|
+
// reuse the same detection — the two paths diverging is the bug class hub#433
|
|
266
|
+
// fixed (smoke 2026-05-27, finding 1). `defaultIsLinkedShared` is imported at
|
|
267
|
+
// module scope; alias kept for the in-function local-shadow convention below.
|
|
268
|
+
const defaultIsLinked = defaultIsLinkedShared;
|
|
282
269
|
|
|
283
270
|
function defaultLinkedPath(pkg: string): string | null {
|
|
284
271
|
// bun has two install shapes for "linked-style" globals:
|
package/src/commands/status.ts
CHANGED
|
@@ -56,9 +56,18 @@ export async function probe(
|
|
|
56
56
|
try {
|
|
57
57
|
const res = await fetchImpl(url, { signal: controller.signal });
|
|
58
58
|
const latencyMs = Math.round(performance.now() - start);
|
|
59
|
+
// A 401 is the service replying "I'm up but this endpoint requires auth"
|
|
60
|
+
// — that's strictly healthy from a liveness perspective. Vault's
|
|
61
|
+
// canonical health path `/vault/<name>/health` is auth-gated; without
|
|
62
|
+
// this carve-out, `parachute status` shows vault as "failing" on every
|
|
63
|
+
// fresh install (first impression UX disaster despite vault being fine).
|
|
64
|
+
// 5xx → unhealthy; 200-class → healthy; 401 → healthy + auth-gated.
|
|
65
|
+
// Other 4xx (404 / 400 / etc.) still count as unhealthy — those mean
|
|
66
|
+
// the configured health path doesn't exist or is shaped wrong.
|
|
67
|
+
const healthy = res.ok || res.status === 401;
|
|
59
68
|
return {
|
|
60
69
|
entry,
|
|
61
|
-
healthy
|
|
70
|
+
healthy,
|
|
62
71
|
statusCode: res.status,
|
|
63
72
|
latencyMs,
|
|
64
73
|
};
|
package/src/help.ts
CHANGED
|
@@ -82,7 +82,7 @@ Environment:
|
|
|
82
82
|
|
|
83
83
|
Examples:
|
|
84
84
|
parachute install vault # installs, runs init, starts vault
|
|
85
|
-
parachute install
|
|
85
|
+
parachute install surface # installs surface (auto-bootstraps Notes)
|
|
86
86
|
parachute install notes # back-compat: legacy notes-daemon (Phase 2 deprecating)
|
|
87
87
|
parachute install scribe # installs, prompts for provider, starts scribe
|
|
88
88
|
parachute install scribe --scribe-provider groq --scribe-key gsk_…
|
|
@@ -188,7 +188,7 @@ Example:
|
|
|
188
188
|
parachute-vault 1940 0.2.4 active 12345 2h 13m 2ms bun-linked → parachute-vault @ 8aa167b
|
|
189
189
|
→ http://127.0.0.1:1940/vault/default/mcp
|
|
190
190
|
parachute-app 1946 0.2.0 active 12346 2h 12m 3ms npm (0.2.0-rc.4)
|
|
191
|
-
→ http://127.0.0.1:1946/
|
|
191
|
+
→ http://127.0.0.1:1946/surface/notes
|
|
192
192
|
`;
|
|
193
193
|
}
|
|
194
194
|
|
package/src/hub-db.ts
CHANGED
|
@@ -278,6 +278,48 @@ const MIGRATIONS: readonly Migration[] = [
|
|
|
278
278
|
UPDATE clients SET same_hub = 0;
|
|
279
279
|
`,
|
|
280
280
|
},
|
|
281
|
+
{
|
|
282
|
+
version: 10,
|
|
283
|
+
sql: `
|
|
284
|
+
-- Multi-user Phase 2 PR 2 (hub#252 follow-up, design
|
|
285
|
+
-- 2026-05-20-multi-user-phase-1.md §Phase 2). Lifts the single
|
|
286
|
+
-- \`users.assigned_vault TEXT\` column into a many-to-many
|
|
287
|
+
-- \`user_vaults\` table so one user can have access to multiple
|
|
288
|
+
-- vaults (e.g. a personal vault + a family-shared vault).
|
|
289
|
+
--
|
|
290
|
+
-- Schema:
|
|
291
|
+
-- * (user_id, vault_name) composite PK — one row per (user, vault).
|
|
292
|
+
-- ON DELETE CASCADE on user_id so user deletion drops the
|
|
293
|
+
-- assignments without us having to clean up manually.
|
|
294
|
+
-- * \`role\` TEXT DEFAULT 'write' — reserved for forward-compat per-
|
|
295
|
+
-- vault role granularity. Phase 1 had no role model; this column
|
|
296
|
+
-- gives later PRs a column to land scope-narrowing in without a
|
|
297
|
+
-- second migration. All backfilled rows default to 'write'.
|
|
298
|
+
-- * index on \`vault_name\` for the inverse lookup ("who has access
|
|
299
|
+
-- to vault X?") — useful when admin removes a vault and we want
|
|
300
|
+
-- to warn about pinned users.
|
|
301
|
+
--
|
|
302
|
+
-- Backfill: every existing row in \`users\` with a non-null
|
|
303
|
+
-- \`assigned_vault\` becomes a single (user_id, vault_name) row in
|
|
304
|
+
-- \`user_vaults\`. Rows with NULL \`assigned_vault\` (admin posture)
|
|
305
|
+
-- get no \`user_vaults\` entry — they remain "no narrowing" per
|
|
306
|
+
-- vaultScopeForUser semantics. After backfill the \`assigned_vault\`
|
|
307
|
+
-- column is dropped.
|
|
308
|
+
CREATE TABLE user_vaults (
|
|
309
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
310
|
+
vault_name TEXT NOT NULL,
|
|
311
|
+
role TEXT NOT NULL DEFAULT 'write',
|
|
312
|
+
created_at TEXT NOT NULL,
|
|
313
|
+
PRIMARY KEY (user_id, vault_name)
|
|
314
|
+
);
|
|
315
|
+
CREATE INDEX user_vaults_vault ON user_vaults (vault_name);
|
|
316
|
+
INSERT INTO user_vaults (user_id, vault_name, role, created_at)
|
|
317
|
+
SELECT id, assigned_vault, 'write', created_at
|
|
318
|
+
FROM users
|
|
319
|
+
WHERE assigned_vault IS NOT NULL;
|
|
320
|
+
ALTER TABLE users DROP COLUMN assigned_vault;
|
|
321
|
+
`,
|
|
322
|
+
},
|
|
281
323
|
];
|
|
282
324
|
|
|
283
325
|
export function openHubDb(path: string = hubDbPath()): Database {
|
package/src/hub-server.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* /admin/login, /admin/logout → 301 → /login, /logout
|
|
24
24
|
*
|
|
25
25
|
* # Notes-as-app migration Phase 2 (parachute-app design doc §16).
|
|
26
|
-
* /notes, /notes/, /notes/* → 301 → /
|
|
26
|
+
* /notes, /notes/, /notes/* → 301 → /surface/notes[/...]
|
|
27
27
|
* (opt-out via
|
|
28
28
|
* hub_settings.notes_redirect_disabled)
|
|
29
29
|
*
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
* /api/users (GET + POST) → list / create user (host:admin)
|
|
66
66
|
* /api/users/vaults (GET) → vault-name list for assigned-vault picker (host:admin)
|
|
67
67
|
* /api/users/<id> (DELETE) → hard-delete user + revoke tokens (host:admin)
|
|
68
|
+
* /api/users/<id>/reset-password (POST) → admin-initiated password reset (host:admin)
|
|
68
69
|
* /login (GET + POST) → operator password login
|
|
69
70
|
* /logout (POST) → end admin session
|
|
70
71
|
* /account/change-password (GET + POST) → user self-service change-password
|
|
@@ -117,7 +118,11 @@ import {
|
|
|
117
118
|
import { handleHostAdminToken } from "./admin-host-admin-token.ts";
|
|
118
119
|
import { handleVaultAdminToken } from "./admin-vault-admin-token.ts";
|
|
119
120
|
import { handleCreateVault } from "./admin-vaults.ts";
|
|
120
|
-
import {
|
|
121
|
+
import {
|
|
122
|
+
handleAccountChangePasswordGet,
|
|
123
|
+
handleAccountChangePasswordPost,
|
|
124
|
+
handleAccountHomeGet,
|
|
125
|
+
} from "./api-account.ts";
|
|
121
126
|
import { handleApiHub } from "./api-hub.ts";
|
|
122
127
|
import { handleApiMe } from "./api-me.ts";
|
|
123
128
|
import { handleApiMintToken } from "./api-mint-token.ts";
|
|
@@ -141,6 +146,8 @@ import {
|
|
|
141
146
|
handleDeleteUser,
|
|
142
147
|
handleListUsers,
|
|
143
148
|
handleListVaults,
|
|
149
|
+
handleResetUserPassword,
|
|
150
|
+
handleUpdateUserVaults,
|
|
144
151
|
} from "./api-users.ts";
|
|
145
152
|
import { buildChromeForRequest, injectChromeIntoResponse } from "./chrome-strip.ts";
|
|
146
153
|
import { CONFIG_DIR, SERVICES_MANIFEST_PATH } from "./config.ts";
|
|
@@ -266,10 +273,7 @@ export function parseArgs(argv: string[], env: NodeJS.ProcessEnv = process.env):
|
|
|
266
273
|
if (hostname === undefined) hostname = env.PARACHUTE_BIND_HOST || "127.0.0.1";
|
|
267
274
|
if (wellKnownDir === undefined) wellKnownDir = WELL_KNOWN_DIR;
|
|
268
275
|
if (issuer === undefined) {
|
|
269
|
-
const fromEnv =
|
|
270
|
-
env.PARACHUTE_HUB_ORIGIN ??
|
|
271
|
-
env.RENDER_EXTERNAL_URL ??
|
|
272
|
-
flyDefaultOrigin(env);
|
|
276
|
+
const fromEnv = env.PARACHUTE_HUB_ORIGIN ?? env.RENDER_EXTERNAL_URL ?? flyDefaultOrigin(env);
|
|
273
277
|
if (fromEnv) issuer = fromEnv.replace(/\/+$/, "") || undefined;
|
|
274
278
|
}
|
|
275
279
|
return { port, hostname, wellKnownDir, dbPath: dbPath ?? hubDbPath(), issuer };
|
|
@@ -1118,8 +1122,7 @@ export function hubFetch(
|
|
|
1118
1122
|
// browser POSTs and must be trusted even when the operator's
|
|
1119
1123
|
// configured issuer points elsewhere. See origin-check.ts
|
|
1120
1124
|
// jsdoc for the failure case this closes.
|
|
1121
|
-
platformOrigin:
|
|
1122
|
-
process.env.RENDER_EXTERNAL_URL ?? flyDefaultOrigin(process.env),
|
|
1125
|
+
platformOrigin: process.env.RENDER_EXTERNAL_URL ?? flyDefaultOrigin(process.env),
|
|
1123
1126
|
}),
|
|
1124
1127
|
};
|
|
1125
1128
|
};
|
|
@@ -1199,7 +1202,7 @@ export function hubFetch(
|
|
|
1199
1202
|
}
|
|
1200
1203
|
|
|
1201
1204
|
// Notes-as-app migration Phase 2 (parachute-app design doc §16).
|
|
1202
|
-
// `/notes/*` 301-redirects to `/
|
|
1205
|
+
// `/notes/*` 301-redirects to `/surface/notes/*` so legacy bookmarks land on
|
|
1203
1206
|
// the apps-hosted Notes. Default-on; operators on notes-as-module-only
|
|
1204
1207
|
// installs can opt out via `hub_settings.notes_redirect_disabled = true`
|
|
1205
1208
|
// (see hub-settings.ts). The opt-out exists so a legacy operator
|
|
@@ -1908,6 +1911,44 @@ export function hubFetch(
|
|
|
1908
1911
|
manifestPath,
|
|
1909
1912
|
});
|
|
1910
1913
|
}
|
|
1914
|
+
// Phase 2 PR 1 — `/api/users/:id/reset-password` (admin-initiated
|
|
1915
|
+
// password reset for non-admin users). Routed BEFORE the per-id DELETE
|
|
1916
|
+
// catch-all so the trailing `/reset-password` segment isn't mistaken
|
|
1917
|
+
// for part of a user id. Same `host:admin` Bearer gate as the other
|
|
1918
|
+
// /api/users surfaces.
|
|
1919
|
+
{
|
|
1920
|
+
const resetMatch = pathname.match(/^\/api\/users\/([^/]+)\/reset-password$/);
|
|
1921
|
+
if (resetMatch) {
|
|
1922
|
+
if (!getDb) return dbNotConfigured();
|
|
1923
|
+
const id = decodeURIComponent(resetMatch[1] ?? "");
|
|
1924
|
+
if (!id) {
|
|
1925
|
+
return new Response("not found", { status: 404 });
|
|
1926
|
+
}
|
|
1927
|
+
return handleResetUserPassword(req, id, {
|
|
1928
|
+
db: getDb(),
|
|
1929
|
+
issuer: oauthDeps(req).issuer,
|
|
1930
|
+
manifestPath,
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
// Phase 2 PR 2 — `/api/users/:id/vaults` (replace a user's vault
|
|
1935
|
+
// assignments). Routed before the per-id DELETE catch-all so the
|
|
1936
|
+
// trailing `/vaults` segment isn't mistaken for part of a user id.
|
|
1937
|
+
{
|
|
1938
|
+
const vaultsMatch = pathname.match(/^\/api\/users\/([^/]+)\/vaults$/);
|
|
1939
|
+
if (vaultsMatch) {
|
|
1940
|
+
if (!getDb) return dbNotConfigured();
|
|
1941
|
+
const id = decodeURIComponent(vaultsMatch[1] ?? "");
|
|
1942
|
+
if (!id) {
|
|
1943
|
+
return new Response("not found", { status: 404 });
|
|
1944
|
+
}
|
|
1945
|
+
return handleUpdateUserVaults(req, id, {
|
|
1946
|
+
db: getDb(),
|
|
1947
|
+
issuer: oauthDeps(req).issuer,
|
|
1948
|
+
manifestPath,
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1911
1952
|
if (pathname.startsWith("/api/users/")) {
|
|
1912
1953
|
if (!getDb) return dbNotConfigured();
|
|
1913
1954
|
const id = decodeURIComponent(pathname.slice("/api/users/".length));
|
|
@@ -1961,6 +2002,24 @@ export function hubFetch(
|
|
|
1961
2002
|
return new Response("method not allowed", { status: 405 });
|
|
1962
2003
|
}
|
|
1963
2004
|
|
|
2005
|
+
// /account/ — friend-facing user home (multi-user Phase 1 follow-up).
|
|
2006
|
+
// Companion to the first-admin gate on `/admin/host-admin-token`: a
|
|
2007
|
+
// signed-in non-admin (friend) lands here instead of bouncing against
|
|
2008
|
+
// a 403 wall on the admin SPA. Admin users also land here when they
|
|
2009
|
+
// hit `/account/` directly, with a "you're the administrator → /admin/"
|
|
2010
|
+
// exit ramp. Bare `/account` 301-redirects to `/account/` so links
|
|
2011
|
+
// without the trailing slash work.
|
|
2012
|
+
if (pathname === "/account" || pathname === "/account/") {
|
|
2013
|
+
if (req.method !== "GET") return new Response("method not allowed", { status: 405 });
|
|
2014
|
+
if (pathname === "/account") {
|
|
2015
|
+
return new Response(null, { status: 301, headers: { location: "/account/" } });
|
|
2016
|
+
}
|
|
2017
|
+
if (!getDb) return dbNotConfigured();
|
|
2018
|
+
const db = getDb();
|
|
2019
|
+
const hubOrigin = resolveIssuer(req, db, configuredIssuer);
|
|
2020
|
+
return handleAccountHomeGet(req, { db, hubOrigin });
|
|
2021
|
+
}
|
|
2022
|
+
|
|
1964
2023
|
// Legacy `/admin/config` (server-rendered module-config portal, #46)
|
|
1965
2024
|
// retired post-SPA-rework. 301 → the SPA home so any bookmark or stale
|
|
1966
2025
|
// post-login redirect lands somewhere useful. The route stays here in
|
|
@@ -2032,7 +2091,7 @@ export function hubFetch(
|
|
|
2032
2091
|
* Inject the persistent chrome strip (workstream G) into a proxied response.
|
|
2033
2092
|
*
|
|
2034
2093
|
* Skips the rewrite when the response is non-200, non-HTML, on an opt-out
|
|
2035
|
-
* path (e.g. `/
|
|
2094
|
+
* path (e.g. `/surface/notes/*`), or larger than `MAX_INJECT_SIZE_BYTES`.
|
|
2036
2095
|
* `injectChromeIntoResponse` is the no-side-effects implementation; this
|
|
2037
2096
|
* wrapper threads in the session-aware chrome HTML and a `set-cookie`
|
|
2038
2097
|
* append when a fresh CSRF cookie was minted.
|
package/src/hub-settings.ts
CHANGED
|
@@ -71,7 +71,7 @@ export type HubSettingKey =
|
|
|
71
71
|
| "hub_origin"
|
|
72
72
|
// Notes-as-app migration Phase 2 (parachute-app design doc §16).
|
|
73
73
|
// When unset (default) or "false", hub serves a 301 redirect from
|
|
74
|
-
// `/notes/*` → `/
|
|
74
|
+
// `/notes/*` → `/surface/notes/*` so existing bookmarks transparently
|
|
75
75
|
// follow the operator to the apps-hosted Notes. When "true", the
|
|
76
76
|
// redirect is skipped and `/notes/*` falls through to the existing
|
|
77
77
|
// services.json-driven proxy — the escape hatch for operators
|
|
@@ -372,7 +372,7 @@ export function setHubOrigin(db: Database, value: string | null): void {
|
|
|
372
372
|
// --- domain helpers: notes-as-app redirect (parachute-app §16 Phase 2) ----
|
|
373
373
|
|
|
374
374
|
/**
|
|
375
|
-
* Read whether the `/notes/*` → `/
|
|
375
|
+
* Read whether the `/notes/*` → `/surface/notes/*` redirect is disabled. Default
|
|
376
376
|
* is `false` (redirect on) — Phase 2 migrates operators to apps-hosted
|
|
377
377
|
* Notes, so the bookmark-friendly path is the default-on behavior. Only an
|
|
378
378
|
* operator running notes-as-a-module without parachute-app installed should
|
package/src/hub.ts
CHANGED
|
@@ -471,9 +471,9 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
471
471
|
* Render the "Get started" section (hub#342) above the Services grid.
|
|
472
472
|
*
|
|
473
473
|
* One hardcoded target, conditional on its prerequisite being installed:
|
|
474
|
-
* - "Open Notes" → /
|
|
475
|
-
*
|
|
476
|
-
* mere presence of
|
|
474
|
+
* - "Open Notes" → /surface/notes/ (requires parachute-surface installed;
|
|
475
|
+
* Surface auto-bootstraps Notes-as-UI per parachute-surface §17, so the
|
|
476
|
+
* mere presence of Surface means /surface/notes/ is live)
|
|
477
477
|
*
|
|
478
478
|
* The earlier "Browse Vault" tile retired in workstream C (2026-05-25)
|
|
479
479
|
* once vault declared uiUrl in its module.json (per patterns#96). With
|
|
@@ -500,12 +500,12 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
500
500
|
function renderGetStarted(services) {
|
|
501
501
|
if (!getStartedGrid || !getStartedSection) return;
|
|
502
502
|
const tiles = [];
|
|
503
|
-
const
|
|
504
|
-
if (
|
|
503
|
+
const hasSurface = services.some((s) => s && s.name === 'parachute-surface');
|
|
504
|
+
if (hasSurface) {
|
|
505
505
|
tiles.push({
|
|
506
506
|
title: 'Open Notes',
|
|
507
507
|
desc: 'Browse + capture in the Notes app — reads from your vault.',
|
|
508
|
-
href: '/
|
|
508
|
+
href: '/surface/notes/',
|
|
509
509
|
});
|
|
510
510
|
}
|
|
511
511
|
if (tiles.length === 0) {
|
package/src/notes-redirect.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Notes-as-app migration Phase 2 (parachute-app design doc §16).
|
|
3
3
|
*
|
|
4
4
|
* When parachute-app ships and Notes installs as `parachute-app add
|
|
5
|
-
* @openparachute/notes-ui --name notes --path /
|
|
5
|
+
* @openparachute/notes-ui --name notes --path /surface/notes`, operators with
|
|
6
6
|
* existing `/notes/*` bookmarks need a transparent bridge. The hub serves a
|
|
7
|
-
* 301 redirect from `/notes/*` → `/
|
|
7
|
+
* 301 redirect from `/notes/*` → `/surface/notes/*` so:
|
|
8
8
|
*
|
|
9
9
|
* - cached operator URLs (notes PWA install banners, browser history,
|
|
10
10
|
* in-vault links) keep working
|
|
@@ -44,14 +44,14 @@ export function isLegacyNotesPath(pathname: string): boolean {
|
|
|
44
44
|
* string. The query is preserved verbatim; the fragment isn't visible
|
|
45
45
|
* server-side (clients reassemble it after following the redirect).
|
|
46
46
|
*
|
|
47
|
-
* The transform is purely path-rewrite — `/notes` → `/
|
|
48
|
-
* → `/
|
|
47
|
+
* The transform is purely path-rewrite — `/notes` → `/surface/notes`, `/notes/`
|
|
48
|
+
* → `/surface/notes/`, `/notes/foo/bar` → `/surface/notes/foo/bar`.
|
|
49
49
|
*/
|
|
50
50
|
export function buildNotesRedirectTarget(pathname: string, search: string): string {
|
|
51
51
|
// Slice off the leading "/notes" — what remains is either "" (bare /notes),
|
|
52
52
|
// "/" (trailing slash), or "/<rest>" (sub-path).
|
|
53
53
|
const tail = pathname.slice("/notes".length);
|
|
54
|
-
return `/
|
|
54
|
+
return `/surface/notes${tail}${search}`;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|