@openparachute/vault 0.5.3-rc.1 → 0.5.3-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/src/core.test.ts +35 -0
- package/core/src/indexed-fields.ts +1 -1
- package/core/src/links.ts +1 -1
- package/core/src/mcp.ts +39 -11
- package/core/src/notes.ts +8 -8
- package/core/src/schema.ts +1 -1
- package/core/src/store.ts +2 -2
- package/core/src/tag-schemas.ts +2 -2
- package/core/src/types.ts +10 -0
- package/core/src/wikilinks.ts +2 -2
- package/package.json +2 -2
- package/src/auth-hub-jwt.test.ts +7 -0
- package/src/hub-jwt.test.ts +75 -2
- package/src/hub-jwt.ts +43 -6
- package/src/routes.ts +34 -14
- package/src/routing.test.ts +7 -1
- package/src/vault.test.ts +40 -0
- package/web/ui/dist/assets/{index-D8nCVT1e.js → index-DJL6Az--.js} +1 -1
- package/web/ui/dist/index.html +1 -1
package/src/routes.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import type { Store, Note } from "../core/src/types.ts";
|
|
15
15
|
import { listUnresolvedWikilinks } from "../core/src/wikilinks.ts";
|
|
16
|
-
import { getNote, getNoteTags, toNoteIndex, filterMetadata, MAX_BATCH_SIZE, validateExtension, ExtensionValidationError } from "../core/src/notes.ts";
|
|
16
|
+
import { getNote, getNotes, getNoteTags, toNoteIndex, filterMetadata, MAX_BATCH_SIZE, validateExtension, ExtensionValidationError } from "../core/src/notes.ts";
|
|
17
17
|
import { attachValidationStatus } from "../core/src/mcp.ts";
|
|
18
18
|
import * as linkOps from "../core/src/links.ts";
|
|
19
19
|
import * as tagSchemaOps from "../core/src/tag-schemas.ts";
|
|
@@ -567,7 +567,7 @@ async function handleNotesInner(
|
|
|
567
567
|
): Promise<Response> {
|
|
568
568
|
const url = new URL(req.url);
|
|
569
569
|
const method = req.method;
|
|
570
|
-
const db =
|
|
570
|
+
const db = store.db;
|
|
571
571
|
|
|
572
572
|
// ---- Collection routes (no ID in path) ----
|
|
573
573
|
if (subpath === "") {
|
|
@@ -1013,19 +1013,33 @@ async function handleNotesInner(
|
|
|
1013
1013
|
throw e;
|
|
1014
1014
|
}
|
|
1015
1015
|
|
|
1016
|
-
// Apply tag schema defaults
|
|
1016
|
+
// Apply tag schema defaults, then re-read the notes whose metadata was
|
|
1017
|
+
// actually default-filled so the response reflects the final on-disk
|
|
1018
|
+
// state (the `created` entries were read before `applySchemaDefaults`
|
|
1019
|
+
// ran). Mirrors the MCP create-note path (vault#316). Batched re-read
|
|
1020
|
+
// (`getNotes` = one `WHERE id IN (...)`), skipped when no defaults
|
|
1021
|
+
// applied so the common no-defaults path adds zero extra reads.
|
|
1022
|
+
const mutatedIds = new Set<string>();
|
|
1017
1023
|
for (const note of created) {
|
|
1018
1024
|
if (note.tags?.length) {
|
|
1019
|
-
await applySchemaDefaults(store, db, [note.id], note.tags)
|
|
1025
|
+
for (const id of await applySchemaDefaults(store, db, [note.id], note.tags)) {
|
|
1026
|
+
mutatedIds.add(id);
|
|
1027
|
+
}
|
|
1020
1028
|
}
|
|
1021
1029
|
}
|
|
1030
|
+
const refreshed =
|
|
1031
|
+
mutatedIds.size === 0
|
|
1032
|
+
? created
|
|
1033
|
+
: (() => {
|
|
1034
|
+
const byId = new Map(getNotes(db, [...mutatedIds]).map((n) => [n.id, n]));
|
|
1035
|
+
return created.map((n) => byId.get(n.id) ?? n);
|
|
1036
|
+
})();
|
|
1022
1037
|
|
|
1023
1038
|
// Attach `validation_status` so HTTP create-note matches the MCP
|
|
1024
|
-
// surface (vault#287).
|
|
1025
|
-
// `core/src/mcp.ts:451`. `attachValidationStatus` returns the note
|
|
1039
|
+
// surface (vault#287). `attachValidationStatus` returns the note
|
|
1026
1040
|
// unchanged when no tag declares fields, so vaults without any tag
|
|
1027
1041
|
// schemas see no behavior change.
|
|
1028
|
-
const final =
|
|
1042
|
+
const final = refreshed.map((n) => attachValidationStatus(store, db, n));
|
|
1029
1043
|
return json(body.notes ? final : final[0], 201);
|
|
1030
1044
|
}
|
|
1031
1045
|
|
|
@@ -1636,7 +1650,7 @@ export async function handleTags(
|
|
|
1636
1650
|
// that token's allowlist. Aggregate matches across sources for a single
|
|
1637
1651
|
// 409 envelope.
|
|
1638
1652
|
const referenced: { source: string; tokens: { id: string; label: string }[] }[] = [];
|
|
1639
|
-
const db =
|
|
1653
|
+
const db = store.db;
|
|
1640
1654
|
for (const src of sources) {
|
|
1641
1655
|
const tokens = findTokensReferencingTag(db, src as string);
|
|
1642
1656
|
if (tokens.length > 0) referenced.push({ source: src as string, tokens });
|
|
@@ -1800,7 +1814,7 @@ export async function handleTags(
|
|
|
1800
1814
|
// tag would silently orphan the token's allowlist. Fail closed (409)
|
|
1801
1815
|
// and name the offending tokens so the operator can revoke or re-mint
|
|
1802
1816
|
// before retrying. patterns/tag-scoped-tokens.md §Dependencies.
|
|
1803
|
-
const referenced_by = findTokensReferencingTag(
|
|
1817
|
+
const referenced_by = findTokensReferencingTag(store.db, tagName);
|
|
1804
1818
|
if (referenced_by.length > 0) {
|
|
1805
1819
|
return json(
|
|
1806
1820
|
{
|
|
@@ -1835,7 +1849,7 @@ export async function handleFindPath(
|
|
|
1835
1849
|
const target = parseQuery(url, "target");
|
|
1836
1850
|
if (!source || !target) return json({ error: "source and target parameters are required" }, 400);
|
|
1837
1851
|
|
|
1838
|
-
const db =
|
|
1852
|
+
const db = store.db;
|
|
1839
1853
|
try {
|
|
1840
1854
|
const sourceNote = await resolveNote(store, source);
|
|
1841
1855
|
if (!sourceNote) return json({ error: `Note not found: "${source}"` }, 404);
|
|
@@ -1957,7 +1971,7 @@ export function handleUnresolvedWikilinks(
|
|
|
1957
1971
|
const url = new URL(req.url);
|
|
1958
1972
|
const limitStr = url.searchParams.get("limit");
|
|
1959
1973
|
const limit = limitStr ? parseInt(limitStr, 10) : 50;
|
|
1960
|
-
const db =
|
|
1974
|
+
const db = store.db;
|
|
1961
1975
|
const result = listUnresolvedWikilinks(db, limit);
|
|
1962
1976
|
|
|
1963
1977
|
// Unscoped token → return as-is (unchanged behavior).
|
|
@@ -2619,9 +2633,12 @@ export async function handleStorage(
|
|
|
2619
2633
|
// Tag schema defaults — same logic as core/src/mcp.ts applySchemaDefaults
|
|
2620
2634
|
// ---------------------------------------------------------------------------
|
|
2621
2635
|
|
|
2622
|
-
|
|
2636
|
+
// Returns the IDs of notes whose metadata was actually default-filled, so
|
|
2637
|
+
// the caller can re-read ONLY the mutated notes (and skip the re-read when
|
|
2638
|
+
// nothing changed). Mirrors the core/src/mcp.ts contract.
|
|
2639
|
+
async function applySchemaDefaults(store: Store, db: any, noteIds: string[], tags: string[]): Promise<string[]> {
|
|
2623
2640
|
const schemas = tagSchemaOps.getTagSchemaMap(db);
|
|
2624
|
-
if (Object.keys(schemas).length === 0) return;
|
|
2641
|
+
if (Object.keys(schemas).length === 0) return [];
|
|
2625
2642
|
|
|
2626
2643
|
const defaults: Record<string, unknown> = {};
|
|
2627
2644
|
for (const tag of tags) {
|
|
@@ -2633,8 +2650,9 @@ async function applySchemaDefaults(store: Store, db: any, noteIds: string[], tag
|
|
|
2633
2650
|
}
|
|
2634
2651
|
}
|
|
2635
2652
|
}
|
|
2636
|
-
if (Object.keys(defaults).length === 0) return;
|
|
2653
|
+
if (Object.keys(defaults).length === 0) return [];
|
|
2637
2654
|
|
|
2655
|
+
const mutated: string[] = [];
|
|
2638
2656
|
for (const noteId of noteIds) {
|
|
2639
2657
|
const note = await store.getNote(noteId);
|
|
2640
2658
|
if (!note) continue;
|
|
@@ -2648,7 +2666,9 @@ async function applySchemaDefaults(store: Store, db: any, noteIds: string[], tag
|
|
|
2648
2666
|
metadata: { ...existing, ...missing },
|
|
2649
2667
|
skipUpdatedAt: true,
|
|
2650
2668
|
});
|
|
2669
|
+
mutated.push(noteId);
|
|
2651
2670
|
}
|
|
2671
|
+
return mutated;
|
|
2652
2672
|
}
|
|
2653
2673
|
|
|
2654
2674
|
function defaultForField(field: { type: string; enum?: string[] }): unknown {
|
package/src/routing.test.ts
CHANGED
|
@@ -149,7 +149,12 @@ function reset(): void {
|
|
|
149
149
|
// Default every test to the fixture hub origin so the hub-JWT mint path
|
|
150
150
|
// resolves JWKS + validates `iss`. Describes that assert the loopback
|
|
151
151
|
// default (OAuth discovery metadata) override this in their own beforeEach.
|
|
152
|
-
if (hubFixtureOrigin)
|
|
152
|
+
if (hubFixtureOrigin) {
|
|
153
|
+
process.env.PARACHUTE_HUB_ORIGIN = hubFixtureOrigin;
|
|
154
|
+
// Post-vault#464 the JWKS fetch origin resolves separately (loopback by
|
|
155
|
+
// default); point it at the fixture so the mint path's JWKS fetch resolves.
|
|
156
|
+
process.env.PARACHUTE_HUB_JWKS_ORIGIN = hubFixtureOrigin;
|
|
157
|
+
}
|
|
153
158
|
resetJwksCache();
|
|
154
159
|
resetRevocationCache();
|
|
155
160
|
}
|
|
@@ -183,6 +188,7 @@ afterAll(() => {
|
|
|
183
188
|
clearVaultStoreCache();
|
|
184
189
|
hubServer?.stop(true);
|
|
185
190
|
delete process.env.PARACHUTE_HUB_ORIGIN;
|
|
191
|
+
delete process.env.PARACHUTE_HUB_JWKS_ORIGIN;
|
|
186
192
|
if (existsSync(testDir)) rmSync(testDir, { recursive: true, force: true });
|
|
187
193
|
});
|
|
188
194
|
|
package/src/vault.test.ts
CHANGED
|
@@ -2023,6 +2023,46 @@ describe("HTTP /notes", async () => {
|
|
|
2023
2023
|
expect(body.createdAt).toBe("2025-01-01T00:00:00.000Z");
|
|
2024
2024
|
});
|
|
2025
2025
|
|
|
2026
|
+
// vault#316 — the HTTP POST path re-reads each note AFTER
|
|
2027
|
+
// `applySchemaDefaults`, so the response metadata carries the just-written
|
|
2028
|
+
// defaults (mirrors the MCP create-note path). Before the fix the response
|
|
2029
|
+
// mapped over the pre-defaults in-memory objects, so default-filled
|
|
2030
|
+
// metadata was missing from `POST /api/notes` responses.
|
|
2031
|
+
test("POST /notes response reflects post-applySchemaDefaults state (vault#316)", async () => {
|
|
2032
|
+
await store.upsertTagSchema("task", {
|
|
2033
|
+
fields: { priority: { type: "string", enum: ["high", "low"] } },
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
// Single: default lands in the returned metadata and agrees with disk.
|
|
2037
|
+
const single = await handleNotes(
|
|
2038
|
+
mkReq("POST", "/notes", { content: "do the thing", path: "Inbox/task-1", tags: ["task"] }),
|
|
2039
|
+
store,
|
|
2040
|
+
"",
|
|
2041
|
+
);
|
|
2042
|
+
expect(single.status).toBe(201);
|
|
2043
|
+
const singleBody = await single.json() as any;
|
|
2044
|
+
expect(singleBody.metadata?.priority).toBe("high"); // first enum value
|
|
2045
|
+
const onDisk = await store.getNoteByPath("Inbox/task-1");
|
|
2046
|
+
expect((onDisk!.metadata as any)?.priority).toBe("high");
|
|
2047
|
+
|
|
2048
|
+
// Batch: each entry is re-read post-defaults too, in input order.
|
|
2049
|
+
const batch = await handleNotes(
|
|
2050
|
+
mkReq("POST", "/notes", {
|
|
2051
|
+
notes: [
|
|
2052
|
+
{ content: "a", path: "Inbox/task-2", tags: ["task"] },
|
|
2053
|
+
{ content: "b", path: "Inbox/task-3", tags: ["task"] },
|
|
2054
|
+
],
|
|
2055
|
+
}),
|
|
2056
|
+
store,
|
|
2057
|
+
"",
|
|
2058
|
+
);
|
|
2059
|
+
expect(batch.status).toBe(201);
|
|
2060
|
+
const batchBody = await batch.json() as any[];
|
|
2061
|
+
expect(batchBody.map((n) => n.path)).toEqual(["Inbox/task-2", "Inbox/task-3"]);
|
|
2062
|
+
expect(batchBody[0].metadata?.priority).toBe("high");
|
|
2063
|
+
expect(batchBody[1].metadata?.priority).toBe("high");
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2026
2066
|
// ---- Extension field (vault#328) ----
|
|
2027
2067
|
|
|
2028
2068
|
test("POST /notes accepts extension and persists it", async () => {
|