@openparachute/vault 0.6.4-rc.8 → 0.6.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.
- package/README.md +6 -4
- package/core/src/bare-tag-guard.test.ts +246 -0
- package/core/src/conformance.test.ts +112 -0
- package/core/src/conformance.ts +214 -0
- package/core/src/mcp.ts +7 -2
- package/core/src/notes.ts +52 -10
- package/core/src/store.ts +65 -2
- package/core/src/tag-hierarchy.ts +26 -0
- package/core/src/types.ts +15 -0
- package/package.json +1 -1
- package/src/bare-tag-guard-routes.test.ts +72 -0
- package/src/cli.ts +6 -6
- package/src/first-boot-create.test.ts +155 -0
- package/src/routes.ts +190 -25
- package/src/routing.test.ts +125 -0
- package/src/routing.ts +10 -2
- package/src/self-register.test.ts +8 -8
- package/src/self-register.ts +9 -5
- package/src/server.ts +58 -38
- package/src/storage.test.ts +75 -8
- package/src/vault-remove.test.ts +4 -5
- package/web/ui/dist/assets/{index-UlvD8KHr.css → index-C5XqQM-c.css} +1 -1
- package/web/ui/dist/assets/index-TJ-XN4OZ.js +61 -0
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-DwYo23aY.js +0 -61
package/src/server.ts
CHANGED
|
@@ -153,17 +153,23 @@ if (process.env.VAULT_AUTH_TOKEN?.trim()) {
|
|
|
153
153
|
// logic lives in auth.ts (import-safe + unit-tested); see warnLegacyGlobalApiKeys.
|
|
154
154
|
warnLegacyGlobalApiKeys(readGlobalConfig().api_keys);
|
|
155
155
|
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
156
|
+
// First-boot vault creation — ONLY when PARACHUTE_VAULT_NAME is explicitly
|
|
157
|
+
// set in the environment. (#478 Part 2)
|
|
158
|
+
//
|
|
159
|
+
// The hub setup wizard and `parachute init --vault-name <name>` both spawn
|
|
160
|
+
// the vault server with PARACHUTE_VAULT_NAME=<operator-chosen-name>; that
|
|
161
|
+
// env var is the load-bearing signal that an external orchestrator has
|
|
162
|
+
// decided what the first vault should be called. With the env var unset the
|
|
163
|
+
// server boots with zero vaults and stays that way — no silent "default"
|
|
164
|
+
// — so a fresh install without a wizard can't accidentally squash an
|
|
165
|
+
// explicit later `parachute-vault create <name>` call.
|
|
160
166
|
//
|
|
161
167
|
// Gated on the `auto_create: false` marker (2026-06-09 hub-module-boundary
|
|
162
168
|
// migration): `parachute-vault remove` writes it when the operator deletes
|
|
163
169
|
// their LAST vault, so an explicit empty-the-server action isn't silently
|
|
164
170
|
// undone by a resurrection with fresh credentials on the next boot. Fresh
|
|
165
|
-
// installs have no config.yaml — no marker — so
|
|
166
|
-
//
|
|
171
|
+
// installs have no config.yaml — no marker — so the env-var-driven path
|
|
172
|
+
// still works on a clean box.
|
|
167
173
|
if (listVaults().length === 0) {
|
|
168
174
|
const globalConfig = readGlobalConfig();
|
|
169
175
|
if (!bootAutoCreateAllowed(globalConfig)) {
|
|
@@ -173,43 +179,57 @@ if (listVaults().length === 0) {
|
|
|
173
179
|
} else if (!globalConfig.default_vault) {
|
|
174
180
|
const firstBoot = resolveFirstBootVaultName(process.env.PARACHUTE_VAULT_NAME);
|
|
175
181
|
if (firstBoot.source === "env") {
|
|
176
|
-
|
|
182
|
+
// PARACHUTE_VAULT_NAME explicitly set + valid — create the named vault.
|
|
183
|
+
console.log(`[vault first-boot] PARACHUTE_VAULT_NAME=${firstBoot.name} — creating vault`);
|
|
184
|
+
const vaultName = firstBoot.name;
|
|
185
|
+
const { fullKey, keyId } = generateApiKey();
|
|
186
|
+
writeVaultConfig({
|
|
187
|
+
name: vaultName,
|
|
188
|
+
api_keys: [{
|
|
189
|
+
id: keyId,
|
|
190
|
+
label: "default",
|
|
191
|
+
scope: "write",
|
|
192
|
+
key_hash: hashKey(fullKey),
|
|
193
|
+
created_at: new Date().toISOString(),
|
|
194
|
+
}],
|
|
195
|
+
created_at: new Date().toISOString(),
|
|
196
|
+
});
|
|
197
|
+
globalConfig.default_vault = vaultName;
|
|
198
|
+
if (!globalConfig.api_keys?.length) {
|
|
199
|
+
globalConfig.api_keys = [{
|
|
200
|
+
id: keyId,
|
|
201
|
+
label: "default",
|
|
202
|
+
scope: "write",
|
|
203
|
+
key_hash: hashKey(fullKey),
|
|
204
|
+
created_at: new Date().toISOString(),
|
|
205
|
+
}];
|
|
206
|
+
}
|
|
207
|
+
writeGlobalConfig(globalConfig);
|
|
208
|
+
console.log(`Auto-created vault "${vaultName}" (API key: ${fullKey})`);
|
|
209
|
+
// Seed the in-vault onboarding guide so a connected AI can self-orient and
|
|
210
|
+
// help set the vault up — same as the `create`/`init` CLI path. Idempotent
|
|
211
|
+
// + best-effort (never fails first boot). Mirrors createVault() in cli.ts.
|
|
212
|
+
await seedOnboardingNotesBestEffort(getVaultStore(vaultName));
|
|
177
213
|
} else if (firstBoot.source === "env-invalid") {
|
|
214
|
+
// PARACHUTE_VAULT_NAME was set but failed validation — do NOT silently
|
|
215
|
+
// fall back to "default". Log the problem and stay empty so the operator
|
|
216
|
+
// sees the misconfiguration instead of a phantom vault.
|
|
178
217
|
console.warn(
|
|
179
|
-
`[vault first-boot] PARACHUTE_VAULT_NAME=${JSON.stringify(firstBoot.rawValue)} is invalid (${firstBoot.reason})
|
|
218
|
+
`[vault first-boot] PARACHUTE_VAULT_NAME=${JSON.stringify(firstBoot.rawValue)} is invalid (${firstBoot.reason}) — server starting with zero vaults. Fix the env var, or create one via the admin wizard, parachute init --vault-name <name>, or parachute-vault create <name>.`,
|
|
180
219
|
);
|
|
181
220
|
} else {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
writeVaultConfig({
|
|
187
|
-
name: vaultName,
|
|
188
|
-
api_keys: [{
|
|
189
|
-
id: keyId,
|
|
190
|
-
label: "default",
|
|
191
|
-
scope: "write",
|
|
192
|
-
key_hash: hashKey(fullKey),
|
|
193
|
-
created_at: new Date().toISOString(),
|
|
194
|
-
}],
|
|
195
|
-
created_at: new Date().toISOString(),
|
|
196
|
-
});
|
|
197
|
-
globalConfig.default_vault = vaultName;
|
|
198
|
-
if (!globalConfig.api_keys?.length) {
|
|
199
|
-
globalConfig.api_keys = [{
|
|
200
|
-
id: keyId,
|
|
201
|
-
label: "default",
|
|
202
|
-
scope: "write",
|
|
203
|
-
key_hash: hashKey(fullKey),
|
|
204
|
-
created_at: new Date().toISOString(),
|
|
205
|
-
}];
|
|
221
|
+
// PARACHUTE_VAULT_NAME unset — stay empty. No silent "default".
|
|
222
|
+
console.log(
|
|
223
|
+
'[vault first-boot] no vaults and PARACHUTE_VAULT_NAME unset — staying empty. Create one via the admin wizard, parachute init --vault-name <name>, or parachute-vault create <name>.',
|
|
224
|
+
);
|
|
206
225
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
226
|
+
} else {
|
|
227
|
+
// Stale config: default_vault is set but no vault DB exists on disk, so
|
|
228
|
+
// first-boot create is skipped. Without a log line the server would boot
|
|
229
|
+
// empty and silent here, which is confusing. (#478 Part 2)
|
|
230
|
+
console.warn(
|
|
231
|
+
'[vault first-boot] config.yaml has default_vault set but no vaults exist on disk — skipping first-boot create. Create one with: parachute-vault create <name>',
|
|
232
|
+
);
|
|
213
233
|
}
|
|
214
234
|
}
|
|
215
235
|
|
package/src/storage.test.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Storage upload
|
|
2
|
+
* Storage upload policy tests (issue #127 origin; vault#517 deny-list).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `/storage
|
|
7
|
-
*
|
|
4
|
+
* `POST /api/storage/upload` accepts ANY file EXCEPT the active-content set a
|
|
5
|
+
* browser can execute when the asset is served back same-origin from
|
|
6
|
+
* `/storage/` (.svg/.html/.htm/.xhtml/.xml/.js/.mjs/.cjs/.css). A knowledge
|
|
7
|
+
* vault stores arbitrary files — ebooks, office docs, datasets, archives,
|
|
8
|
+
* binaries — so the long tail (.epub/.csv/.zip/.exe/…) is accepted, not
|
|
9
|
+
* rejected. We pin the accepted breadth AND the deliberate blocklist so a
|
|
10
|
+
* future edit can't quietly let SVG/HTML in (or quietly start rejecting docs).
|
|
11
|
+
* Two guards keep served files inert: every non-curated type serves as
|
|
12
|
+
* application/octet-stream, and the GET serve path pins
|
|
13
|
+
* `X-Content-Type-Options: nosniff`.
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
@@ -91,6 +97,43 @@ describe("storage upload allowlist", () => {
|
|
|
91
97
|
}
|
|
92
98
|
});
|
|
93
99
|
|
|
100
|
+
test("accepts .epub — the reported gap (ebooks are knowledge content)", async () => {
|
|
101
|
+
const res = await handleStorage(uploadRequest("book.epub", "application/epub+zip"), "/upload", "default", uploadStore);
|
|
102
|
+
expect(res.status).toBe(201);
|
|
103
|
+
const body = (await res.json()) as { mimeType: string; path: string };
|
|
104
|
+
expect(body.mimeType).toBe("application/epub+zip");
|
|
105
|
+
expect(body.path).toMatch(/\.epub$/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("accepts common document / text / data / archive types", async () => {
|
|
109
|
+
for (const [name, mime, expected] of [
|
|
110
|
+
["notes.txt", "text/plain", "text/plain; charset=utf-8"],
|
|
111
|
+
["readme.md", "text/markdown", "text/markdown; charset=utf-8"],
|
|
112
|
+
["data.csv", "text/csv", "text/csv; charset=utf-8"],
|
|
113
|
+
["data.json", "application/json", "application/json; charset=utf-8"],
|
|
114
|
+
["doc.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
|
|
115
|
+
["archive.zip", "application/zip", "application/zip"],
|
|
116
|
+
["clip.mov", "video/quicktime", "video/quicktime"],
|
|
117
|
+
["photo.heic", "image/heic", "image/heic"],
|
|
118
|
+
] as const) {
|
|
119
|
+
const res = await handleStorage(uploadRequest(name, mime), "/upload", "default", uploadStore);
|
|
120
|
+
expect(res.status).toBe(201);
|
|
121
|
+
const body = (await res.json()) as { mimeType: string };
|
|
122
|
+
expect(body.mimeType).toBe(expected);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("accepts arbitrary / unknown files — served as octet-stream (download), never run", async () => {
|
|
127
|
+
// Deny-list policy (vault#517): anything not in the active-content blocklist
|
|
128
|
+
// is accepted. Unknown/no-MIME extensions serve as octet-stream — a download.
|
|
129
|
+
for (const name of ["deck.pages", "tool.exe", "data.bin", "Makefile", "archive.tar.gz"] as const) {
|
|
130
|
+
const res = await handleStorage(uploadRequest(name, "application/octet-stream"), "/upload", "default", uploadStore);
|
|
131
|
+
expect(res.status).toBe(201);
|
|
132
|
+
const body = (await res.json()) as { mimeType: string };
|
|
133
|
+
expect(body.mimeType).toBe("application/octet-stream");
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
94
137
|
test("rejects .svg — XSS vector via inline <script> (#127)", async () => {
|
|
95
138
|
const res = await handleStorage(uploadRequest("evil.svg", "image/svg+xml"), "/upload", "default", uploadStore);
|
|
96
139
|
expect(res.status).toBe(400);
|
|
@@ -105,9 +148,31 @@ describe("storage upload allowlist", () => {
|
|
|
105
148
|
expect(body.error).toContain(".html");
|
|
106
149
|
});
|
|
107
150
|
|
|
108
|
-
test("rejects
|
|
109
|
-
|
|
110
|
-
|
|
151
|
+
test("rejects the active-content set (.js/.mjs/.cjs/.xhtml/.htm/.xml/.css/.shtml)", async () => {
|
|
152
|
+
for (const [name, mime] of [
|
|
153
|
+
["script.js", "text/javascript"],
|
|
154
|
+
["mod.mjs", "text/javascript"],
|
|
155
|
+
["mod.cjs", "text/javascript"],
|
|
156
|
+
["page.xhtml", "application/xhtml+xml"],
|
|
157
|
+
["page.xht", "application/xhtml+xml"],
|
|
158
|
+
["page.htm", "text/html"],
|
|
159
|
+
["page.shtml", "text/html"],
|
|
160
|
+
["feed.xml", "application/xml"],
|
|
161
|
+
["style.css", "text/css"],
|
|
162
|
+
] as const) {
|
|
163
|
+
const res = await handleStorage(uploadRequest(name, mime), "/upload", "default", uploadStore);
|
|
164
|
+
expect(res.status).toBe(400);
|
|
165
|
+
const body = (await res.json()) as { error: string };
|
|
166
|
+
expect(body.error).toContain("not allowed");
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("trailing-dot / trailing-space can't slip a blocked type past the guard", async () => {
|
|
171
|
+
// extname("evil.html.") === "." — normalize the trailing run first.
|
|
172
|
+
for (const name of ["evil.html.", "evil.svg ", "evil.js."] as const) {
|
|
173
|
+
const res = await handleStorage(uploadRequest(name, "text/plain"), "/upload", "default", uploadStore);
|
|
174
|
+
expect(res.status).toBe(400);
|
|
175
|
+
}
|
|
111
176
|
});
|
|
112
177
|
});
|
|
113
178
|
|
|
@@ -166,6 +231,8 @@ describe("storage GET tag-scope enforcement", () => {
|
|
|
166
231
|
const res = await handleStorage(getReq(inScopePath), `/${inScopePath}`, VAULT, store, ctx);
|
|
167
232
|
expect(res.status).toBe(200);
|
|
168
233
|
expect(res.headers.get("Content-Type")).toBe("application/pdf");
|
|
234
|
+
// Served bytes pin nosniff so a browser can't sniff them into an active type.
|
|
235
|
+
expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
|
|
169
236
|
expect((await res.arrayBuffer()).byteLength).toBe(4);
|
|
170
237
|
});
|
|
171
238
|
|
package/src/vault-remove.test.ts
CHANGED
|
@@ -141,13 +141,12 @@ describe("vault remove — last-vault auto_create marker", () => {
|
|
|
141
141
|
// resurrection of a freshly-credentialed "default".
|
|
142
142
|
expect(bootAutoCreateAllowed(config)).toBe(false);
|
|
143
143
|
|
|
144
|
-
// services.json was still refreshed: with zero vaults the row
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
// registration shape (and the hub still sees the module as installed).
|
|
144
|
+
// services.json was still refreshed: with zero vaults the row carries
|
|
145
|
+
// paths: [] (#478) — the row stays present so the hub sees the module as
|
|
146
|
+
// installed, but advertises no /vault/<name> path (no phantom default).
|
|
148
147
|
const vault = readServices(home).find((s) => s.name === "parachute-vault");
|
|
149
148
|
expect(vault).toBeDefined();
|
|
150
|
-
expect(vault!.paths).toEqual([
|
|
149
|
+
expect(vault!.paths).toEqual([]);
|
|
151
150
|
});
|
|
152
151
|
|
|
153
152
|
test("removing a NON-last vault does not write the marker", () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--bg: #faf8f4;--bg-soft: #f3f0ea;--fg: #2c2a26;--fg-muted: #6b6860;--fg-dim: #9a9690;--accent: #4a7c59;--accent-soft: rgba(74, 124, 89, .08);--accent-hover: #3d6849;--border: #e4e0d8;--border-light: #ece9e2;--card-bg: #ffffff;--error: #a3392b;--error-soft: rgba(163, 57, 43, .08);--warn: #b08023;--warn-soft: rgba(176, 128, 35, .08);--success: #3d6849;--success-soft: rgba(61, 104, 73, .08);--font-serif: Georgia, "Times New Roman", serif;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--font-mono: ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Mono", monospace;font-family:var(--font-sans)}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg);color:var(--fg)}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}code{font-family:var(--font-mono);font-size:.85em;background:var(--bg-soft);padding:.1em .3em;border-radius:3px}.page{max-width:880px;margin:0 auto;padding:1.5rem 1.5rem 6rem}.nav{display:flex;gap:1rem;align-items:center;padding-bottom:1rem;border-bottom:1px solid var(--border);margin-bottom:2rem}.nav .brand{font-weight:600;font-family:var(--font-serif);font-size:1.15rem;margin-right:auto}.nav .brand .sub{color:var(--fg-dim);font-size:.78rem;font-weight:400;margin-left:.4rem;font-family:var(--font-sans)}.nav a{color:var(--fg-muted);font-size:.95rem}.nav a:hover{text-decoration:none;color:var(--fg)}h2{margin:0 0 1rem;font-size:1.4rem;font-weight:500}.muted{color:var(--fg-muted);font-size:.92rem}.dim{color:var(--fg-dim);font-size:.85rem}.error-banner{background:var(--error-soft);border:1px solid var(--error);color:var(--error);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.warn-banner{background:var(--warn-soft);border:1px solid var(--warn);color:var(--warn);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.empty{padding:3rem 1.5rem;text-align:center;color:var(--fg-muted);background:var(--bg-soft);border-radius:10px}.list-header{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin-bottom:1rem}.list-header h2{margin:0}.vault-row{display:flex;align-items:center;gap:1rem;padding:.85rem 1rem;background:#fff;border:1px solid var(--border);border-radius:8px;margin-bottom:.5rem;text-decoration:none;color:inherit;transition:border-color .15s ease}.vault-row:hover{border-color:var(--accent);text-decoration:none}.vault-row .body{flex:1;min-width:0}.vault-row .name{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.vault-row .name code{font-size:.95em}.vault-row .chev{color:var(--fg-dim);font-size:1.2rem}.vault-row-group{margin-bottom:.5rem}.vault-row-group .vault-row{margin-bottom:0}.vault-row-actions{display:flex;align-items:center;gap:.5rem;flex-shrink:0}.vault-usage{font-size:.85rem}.field-error{color:#b3402a;font-size:.85rem;margin-top:.35rem}.section{background:#fff;border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:1.5rem}.kv{display:grid;grid-template-columns:8.5rem 1fr;gap:.5rem 1rem;font-size:.92rem}.kv>div:nth-child(odd){color:var(--fg-muted)}.kv code{word-break:break-all}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(8rem,1fr));gap:1rem;font-size:.92rem}.stat{background:var(--bg-soft);border-radius:8px;padding:.85rem 1rem}.stat .label{color:var(--fg-muted);font-size:.82rem;margin-bottom:.2rem}.stat .value{font-size:1.4rem;font-weight:500;font-family:var(--font-mono)}button{font:inherit;background:var(--accent);color:#fff;border:0;border-radius:6px;padding:.55rem 1.1rem;cursor:pointer;transition:background .15s ease}button:hover{background:var(--accent-hover)}button:disabled{opacity:.5;cursor:not-allowed}button.secondary{background:#fff;color:var(--fg);border:1px solid var(--border)}button.secondary:hover{background:var(--bg-soft)}input,select{font:inherit;background:#fff;color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:.5rem .7rem;width:100%;max-width:28rem}input:focus,select:focus{outline:2px solid var(--accent-soft);border-color:var(--accent)}.form-row{margin-bottom:1rem}.form-row label{display:block;font-size:.88rem;color:var(--fg-muted);margin-bottom:.3rem}.actions{display:flex;gap:.5rem;margin-top:1rem}.manage-list{margin:0;padding-left:1.1rem;font-size:.95rem}.manage-list li{margin-bottom:.4rem}.mint-banner{background:var(--success-soft);border:1px solid var(--success);border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:1.5rem}.mint-banner h3{margin:0 0 .5rem;font-size:1rem;color:var(--success)}.mint-banner .token-box{display:flex;align-items:center;gap:.5rem;margin:.85rem 0 .5rem}.mint-banner code{flex:1;font-size:.9rem;padding:.6rem .75rem;background:#fff;border:1px solid var(--border);word-break:break-all;-webkit-user-select:all;user-select:all}.mint-banner .warn{margin:.75rem 0 0;font-size:.85rem;color:var(--warn)}.mint-banner .actions{margin-top:1rem}.mcp-connect-card h3{margin:0 0 .4rem;font-size:1rem;font-weight:500}.mcp-connect-card>p{margin-top:0}.mcp-connect-card .token-box{display:flex;align-items:center;gap:.5rem;margin:.35rem 0 .25rem}.mcp-connect-card .token-box code{flex:1;font-size:.85rem;padding:.55rem .7rem;background:var(--bg-soft);border:1px solid var(--border);border-radius:6px;word-break:break-all;-webkit-user-select:all;user-select:all}.mcp-field{margin-top:.9rem}.mcp-field-label{display:block;font-size:.82rem;font-weight:600;color:var(--fg-muted)}.mcp-field .dim{margin:.3rem 0 0}.mcp-docs-link{margin:.9rem 0 0}.token-list{display:flex;flex-direction:column;gap:.5rem}.token-row{display:flex;align-items:flex-start;gap:1rem;padding:.85rem 1rem;background:#fff;border:1px solid var(--border);border-radius:8px}.token-row .body{flex:1;min-width:0}.token-row .name{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin-bottom:.3rem}.token-row .meta{font-size:.85rem;margin-bottom:.2rem}.token-row .actions{margin-top:0;flex-shrink:0}.scope-tag{font-size:.78rem;padding:.1em .4em;margin-right:.3em}.tag-pill{background:var(--accent-soft, rgba(120, 120, 200, .12))}.tag-picker{display:flex;flex-wrap:wrap;gap:.4rem .8rem;margin-top:.35rem;max-height:9rem;overflow-y:auto;padding:.4rem .5rem;border:1px solid var(--border, #d8d8de);border-radius:4px}.tag-checkbox{display:inline-flex;align-items:center;gap:.3rem;font-size:.85rem;cursor:pointer;-webkit-user-select:none;user-select:none}.tag-checkbox input[type=checkbox]{margin:0}.preset-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:.75rem;margin-top:.35rem}.preset-card{background:var(--bg-soft);color:var(--fg);border:1px solid var(--border);border-radius:8px;padding:.85rem 1rem;text-align:left;display:flex;flex-direction:column;gap:.25rem;cursor:pointer;transition:border-color .15s ease,background .15s ease}.preset-card:hover{border-color:var(--accent);background:#fff}.preset-card:disabled{opacity:.5;cursor:not-allowed}.preset-card strong{font-size:.95rem;font-weight:500}.preset-card .dim{font-size:.8rem;line-height:1.35}.radio-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.35rem;font-size:.92rem;cursor:pointer}.radio-row input[type=radio]{margin:0}.info-banner{background:var(--accent-soft);border:1px solid var(--accent);color:var(--accent-hover);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000059;display:flex;align-items:center;justify-content:center;z-index:100;padding:1rem}.modal{background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:1.5rem;width:min(520px,100%);max-height:90vh;overflow-y:auto;box-shadow:0 10px 40px #0003}.device-code-row{display:flex;align-items:center;gap:.75rem;margin:.75rem 0}.device-code{font-size:1.4rem;letter-spacing:.1em;padding:.6rem 1rem;background:var(--bg-soft);border-radius:8px;border:1px solid var(--border);font-family:ui-monospace,SF Mono,Menlo,monospace;-webkit-user-select:all;user-select:all;flex:1;text-align:center}.repo-list{display:flex;flex-direction:column;gap:.35rem;max-height:260px;overflow-y:auto;border:1px solid var(--border);border-radius:8px;padding:.4rem;background:var(--bg-soft)}.repo-row{background:var(--bg);border:1px solid transparent;border-radius:6px;padding:.55rem .75rem;font-size:.92rem;display:flex;align-items:baseline;justify-content:space-between;gap:1rem;cursor:pointer;text-align:left;color:inherit;transition:border-color .12s ease,background .12s ease}.repo-row:hover{border-color:var(--accent);background:#fff}.repo-row:disabled{opacity:.5;cursor:not-allowed}
|
|
1
|
+
:root{--bg: #faf8f4;--bg-soft: #f3f0ea;--fg: #2c2a26;--fg-muted: #6b6860;--fg-dim: #9a9690;--accent: #4a7c59;--accent-soft: rgba(74, 124, 89, .08);--accent-hover: #3d6849;--border: #e4e0d8;--border-light: #ece9e2;--card-bg: #ffffff;--error: #a3392b;--error-soft: rgba(163, 57, 43, .08);--warn: #b08023;--warn-soft: rgba(176, 128, 35, .08);--success: #3d6849;--success-soft: rgba(61, 104, 73, .08);--font-serif: Georgia, "Times New Roman", serif;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--font-mono: ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Mono", monospace;font-family:var(--font-sans)}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg);color:var(--fg)}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}code{font-family:var(--font-mono);font-size:.85em;background:var(--bg-soft);padding:.1em .3em;border-radius:3px}.page{max-width:880px;margin:0 auto;padding:1.5rem 1.5rem 6rem}.nav{display:flex;gap:1rem;align-items:center;padding-bottom:1rem;border-bottom:1px solid var(--border);margin-bottom:2rem}.nav .brand{font-weight:600;font-family:var(--font-serif);font-size:1.15rem;margin-right:auto}.nav .brand .sub{color:var(--fg-dim);font-size:.78rem;font-weight:400;margin-left:.4rem;font-family:var(--font-sans)}.nav a{color:var(--fg-muted);font-size:.95rem}.nav a:hover{text-decoration:none;color:var(--fg)}h2{margin:0 0 1rem;font-size:1.4rem;font-weight:500}.muted{color:var(--fg-muted);font-size:.92rem}.dim{color:var(--fg-dim);font-size:.85rem}.error-banner{background:var(--error-soft);border:1px solid var(--error);color:var(--error);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.warn-banner{background:var(--warn-soft);border:1px solid var(--warn);color:var(--warn);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.empty{padding:3rem 1.5rem;text-align:center;color:var(--fg-muted);background:var(--bg-soft);border-radius:10px}.list-header{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin-bottom:1rem}.list-header h2{margin:0}.vault-row{display:flex;align-items:center;gap:1rem;padding:.85rem 1rem;background:#fff;border:1px solid var(--border);border-radius:8px;margin-bottom:.5rem;text-decoration:none;color:inherit;transition:border-color .15s ease}.vault-row:hover{border-color:var(--accent);text-decoration:none}.vault-row .body{flex:1;min-width:0}.vault-row .name{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.vault-row .name code{font-size:.95em}.vault-row .chev{color:var(--fg-dim);font-size:1.2rem}.vault-row-group{margin-bottom:.5rem}.vault-row-group .vault-row{margin-bottom:0}.vault-row-actions{display:flex;align-items:center;gap:.5rem;flex-shrink:0}.vault-usage{font-size:.85rem}.field-error{color:#b3402a;font-size:.85rem;margin-top:.35rem}.section{background:#fff;border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:1.5rem}.kv{display:grid;grid-template-columns:8.5rem 1fr;gap:.5rem 1rem;font-size:.92rem}.kv>div:nth-child(odd){color:var(--fg-muted)}.kv code{word-break:break-all}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(8rem,1fr));gap:1rem;font-size:.92rem}.stat{background:var(--bg-soft);border-radius:8px;padding:.85rem 1rem}.stat .label{color:var(--fg-muted);font-size:.82rem;margin-bottom:.2rem}.stat .value{font-size:1.4rem;font-weight:500;font-family:var(--font-mono)}button{font:inherit;background:var(--accent);color:#fff;border:0;border-radius:6px;padding:.55rem 1.1rem;cursor:pointer;transition:background .15s ease}button:hover{background:var(--accent-hover)}button:disabled{opacity:.5;cursor:not-allowed}button.secondary{background:#fff;color:var(--fg);border:1px solid var(--border)}button.secondary:hover{background:var(--bg-soft)}input,select{font:inherit;background:#fff;color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:.5rem .7rem;width:100%;max-width:28rem}input:focus,select:focus{outline:2px solid var(--accent-soft);border-color:var(--accent)}.form-row{margin-bottom:1rem}.form-row label{display:block;font-size:.88rem;color:var(--fg-muted);margin-bottom:.3rem}.actions{display:flex;gap:.5rem;margin-top:1rem}.manage-list{margin:0;padding-left:1.1rem;font-size:.95rem}.manage-list li{margin-bottom:.4rem}.mint-banner{background:var(--success-soft);border:1px solid var(--success);border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:1.5rem}.mint-banner h3{margin:0 0 .5rem;font-size:1rem;color:var(--success)}.mint-banner .token-box{display:flex;align-items:center;gap:.5rem;margin:.85rem 0 .5rem}.mint-banner code{flex:1;font-size:.9rem;padding:.6rem .75rem;background:#fff;border:1px solid var(--border);word-break:break-all;-webkit-user-select:all;user-select:all}.mint-banner .warn{margin:.75rem 0 0;font-size:.85rem;color:var(--warn)}.mint-banner .actions{margin-top:1rem}.mcp-connect-card h3{margin:0 0 .4rem;font-size:1rem;font-weight:500}.mcp-connect-card>p{margin-top:0}.mcp-connect-card .token-box{display:flex;align-items:center;gap:.5rem;margin:.35rem 0 .25rem}.mcp-connect-card .token-box code{flex:1;font-size:.85rem;padding:.55rem .7rem;background:var(--bg-soft);border:1px solid var(--border);border-radius:6px;word-break:break-all;-webkit-user-select:all;user-select:all}.mcp-field{margin-top:.9rem}.mcp-field-label{display:block;font-size:.82rem;font-weight:600;color:var(--fg-muted)}.mcp-field .dim{margin:.3rem 0 0}.mcp-docs-link{margin:.9rem 0 0}.token-list{display:flex;flex-direction:column;gap:.5rem}.token-row{display:flex;align-items:flex-start;gap:1rem;padding:.85rem 1rem;background:#fff;border:1px solid var(--border);border-radius:8px}.token-row .body{flex:1;min-width:0}.token-row .name{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin-bottom:.3rem}.token-row .meta{font-size:.85rem;margin-bottom:.2rem}.token-row .actions{margin-top:0;flex-shrink:0}.scope-tag{font-size:.78rem;padding:.1em .4em;margin-right:.3em}.tag-pill{background:var(--accent-soft, rgba(120, 120, 200, .12))}.tag-picker{display:flex;flex-wrap:wrap;gap:.4rem .8rem;margin-top:.35rem;max-height:9rem;overflow-y:auto;padding:.4rem .5rem;border:1px solid var(--border, #d8d8de);border-radius:4px}.tag-checkbox{display:inline-flex;align-items:center;gap:.3rem;font-size:.85rem;cursor:pointer;-webkit-user-select:none;user-select:none}.tag-checkbox input[type=checkbox]{margin:0}.preset-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:.75rem;margin-top:.35rem}.preset-card{background:var(--bg-soft);color:var(--fg);border:1px solid var(--border);border-radius:8px;padding:.85rem 1rem;text-align:left;display:flex;flex-direction:column;gap:.25rem;cursor:pointer;transition:border-color .15s ease,background .15s ease}.preset-card:hover{border-color:var(--accent);background:#fff}.preset-card:disabled{opacity:.5;cursor:not-allowed}.preset-card strong{font-size:.95rem;font-weight:500}.preset-card .dim{font-size:.8rem;line-height:1.35}.radio-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.35rem;font-size:.92rem;cursor:pointer}.radio-row input[type=radio]{margin:0}.info-banner{background:var(--accent-soft);border:1px solid var(--accent);color:var(--accent-hover);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000059;display:flex;align-items:center;justify-content:center;z-index:100;padding:1rem}.modal{background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:1.5rem;width:min(520px,100%);max-height:90vh;overflow-y:auto;box-shadow:0 10px 40px #0003}.device-code-row{display:flex;align-items:center;gap:.75rem;margin:.75rem 0}.device-code{font-size:1.4rem;letter-spacing:.1em;padding:.6rem 1rem;background:var(--bg-soft);border-radius:8px;border:1px solid var(--border);font-family:ui-monospace,SF Mono,Menlo,monospace;-webkit-user-select:all;user-select:all;flex:1;text-align:center}.repo-list{display:flex;flex-direction:column;gap:.35rem;max-height:260px;overflow-y:auto;border:1px solid var(--border);border-radius:8px;padding:.4rem;background:var(--bg-soft)}.repo-row{background:var(--bg);border:1px solid transparent;border-radius:6px;padding:.55rem .75rem;font-size:.92rem;display:flex;align-items:baseline;justify-content:space-between;gap:1rem;cursor:pointer;text-align:left;color:inherit;transition:border-color .12s ease,background .12s ease}.repo-row:hover{border-color:var(--accent);background:#fff}.repo-row:disabled{opacity:.5;cursor:not-allowed}.schema-layout{display:grid;grid-template-columns:minmax(180px,240px) 1fr;gap:1.25rem;align-items:start}@media(max-width:720px){.schema-layout{grid-template-columns:1fr}}.schema-taglist{border:1px solid var(--border);border-radius:10px;padding:.5rem;background:var(--bg-soft);max-height:70vh;overflow-y:auto}.tag-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.25rem}.tag-list-item{width:100%;display:flex;flex-direction:column;gap:.2rem;align-items:flex-start;background:transparent;border:1px solid transparent;border-radius:7px;padding:.4rem .5rem;cursor:pointer;text-align:left;color:inherit;font:inherit}.tag-list-item:hover{background:#fff;border-color:var(--border)}.tag-list-item.selected{background:var(--accent-soft);border-color:var(--accent)}.tag-list-item .tag-name{font-size:.9rem}.tag-meta{display:flex;flex-wrap:wrap;gap:.3rem;align-items:center}.badge{display:inline-block;font-size:.7rem;line-height:1.4;padding:0 .4rem;border-radius:999px;background:var(--accent-soft);color:var(--accent);border:1px solid var(--accent)}.badge-strict{background:var(--error-soft);color:var(--error);border-color:var(--error)}.schema-detail{border:1px solid var(--border);border-radius:10px;padding:1rem 1.25rem;min-width:0}.schema-fields,.schema-parents{border:1px solid var(--border);border-radius:8px;padding:.75rem;margin:1rem 0}.schema-fields legend,.schema-parents legend{font-weight:600;font-size:.9rem;padding:0 .4rem}.field-row{border-bottom:1px solid var(--border-light);padding:.5rem 0}.field-row:last-of-type{border-bottom:none}.field-row-main{display:flex;flex-wrap:wrap;gap:.5rem;align-items:center}.field-name-input{flex:1 1 140px;min-width:120px}.field-row-main select{flex:0 0 auto;width:auto}.field-flag{display:inline-flex;align-items:center;gap:.25rem;font-size:.85rem;white-space:nowrap}.field-flag input{width:auto;margin:0}.field-remove{padding:.2rem .5rem;line-height:1}.field-row-extra{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.4rem}.field-enum-input,.field-desc-input{flex:1 1 200px;min-width:160px}.field-codeclarers{margin:.35rem 0 0}.field-codeclarers code{margin-left:.25rem}.parent-grid{display:flex;flex-wrap:wrap;gap:.4rem}.parent-chip{display:inline-flex;align-items:center;gap:.3rem;border:1px solid var(--border);border-radius:999px;padding:.15rem .55rem;font-size:.85rem}.parent-chip input{width:auto;margin:0}.schema-table{width:100%;border-collapse:collapse;font-size:.88rem}.schema-table th,.schema-table td{text-align:left;padding:.3rem .5rem;border-bottom:1px solid var(--border-light)}.schema-table th{font-weight:600;color:var(--fg-muted)}.schema-table td code{margin-right:.3rem}.schema-table .tag-self{background:var(--accent-soft);border-radius:4px;padding:0 .2rem}.schema-table .row-this-tag td{background:var(--accent-soft)}
|