@openparachute/vault 0.5.1 → 0.5.2-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/core/src/core.test.ts +183 -26
- package/core/src/expand-visibility.test.ts +102 -0
- package/core/src/expand.ts +31 -3
- package/core/src/link-count.test.ts +301 -0
- package/core/src/links.ts +77 -0
- package/core/src/mcp.ts +130 -22
- package/core/src/notes.ts +36 -0
- package/core/src/portable-md.test.ts +40 -0
- package/core/src/schema.ts +7 -4
- package/core/src/store.ts +1 -1
- package/core/src/tag-schemas.ts +59 -44
- package/core/src/types.ts +31 -3
- package/package.json +1 -1
- package/src/auth.test.ts +37 -1
- package/src/auth.ts +29 -0
- package/src/cli.ts +286 -68
- package/src/config.test.ts +16 -0
- package/src/config.ts +39 -0
- package/src/init-summary.test.ts +77 -5
- package/src/init-summary.ts +37 -19
- package/src/mcp-tools.ts +60 -6
- package/src/routes.ts +486 -53
- package/src/routing.test.ts +185 -0
- package/src/routing.ts +32 -2
- package/src/server.ts +7 -0
- package/src/storage.test.ts +162 -0
- package/src/tag-scope.ts +68 -1
- package/src/transcription-worker.test.ts +471 -5
- package/src/transcription-worker.ts +212 -44
- package/src/usage.test.ts +362 -0
- package/src/usage.ts +318 -0
- package/src/vault-create.test.ts +298 -11
- package/src/vault.test.ts +1064 -7
package/core/src/types.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { TagFieldSchema, TagRelationship, TagRecord } from "./tag-schemas.js";
|
|
1
|
+
import type { TagFieldSchema, TagRelationship, TagRelationshipMap, TagRecord } from "./tag-schemas.js";
|
|
2
2
|
import type { PrunedField } from "./indexed-fields.js";
|
|
3
3
|
|
|
4
4
|
// ---- Re-exports ----
|
|
5
5
|
|
|
6
|
-
export type { TagFieldSchema, TagRelationship, TagRecord } from "./tag-schemas.js";
|
|
6
|
+
export type { TagFieldSchema, TagRelationship, TagRelationshipMap, TagRecord } from "./tag-schemas.js";
|
|
7
7
|
export type { PrunedField } from "./indexed-fields.js";
|
|
8
8
|
|
|
9
9
|
// ---- Note ----
|
|
@@ -25,6 +25,14 @@ export interface Note {
|
|
|
25
25
|
updatedAt?: string;
|
|
26
26
|
tags?: string[];
|
|
27
27
|
links?: Link[];
|
|
28
|
+
/**
|
|
29
|
+
* Opt-in link degree (raw row count, both directions by default). Present
|
|
30
|
+
* only when the caller requests it via `include_link_count` (REST/MCP).
|
|
31
|
+
* Surfaced the same way `links`/`attachments` are — an extra key injected
|
|
32
|
+
* onto the response after the base shape. See `getLinkCounts` in links.ts
|
|
33
|
+
* for the exact degree semantics (self-loop = 2 under `both`).
|
|
34
|
+
*/
|
|
35
|
+
linkCount?: number;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
// ---- Link ----
|
|
@@ -59,6 +67,16 @@ export interface VaultStats {
|
|
|
59
67
|
tagCount: number;
|
|
60
68
|
attachmentCount: number;
|
|
61
69
|
linkCount: number;
|
|
70
|
+
/**
|
|
71
|
+
* Total bytes of all note content, computed as the sum of the UTF-8 byte
|
|
72
|
+
* length of every note's `content`. The SQL uses `LENGTH(CAST(content AS
|
|
73
|
+
* BLOB))` deliberately: SQLite's bare `LENGTH(text)` returns the number of
|
|
74
|
+
* *characters*, not bytes, so a note full of multibyte UTF-8 (emoji, CJK,
|
|
75
|
+
* accents) would undercount its true on-disk/on-wire footprint. Casting to
|
|
76
|
+
* BLOB forces `LENGTH` to count raw bytes. This is the logical content size,
|
|
77
|
+
* not the physical DB-file size (see `usage.ts:dbBytes` for the latter).
|
|
78
|
+
*/
|
|
79
|
+
contentBytes: number;
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
// ---- Query Options ----
|
|
@@ -115,6 +133,12 @@ export interface QueryOpts {
|
|
|
115
133
|
// declared `indexed: true`; errors loudly otherwise. Direction is taken
|
|
116
134
|
// from `sort` (default "asc") and `created_at` is appended as a stable
|
|
117
135
|
// tiebreaker.
|
|
136
|
+
//
|
|
137
|
+
// The pseudo-field `link_count` is special-cased (no indexed-field
|
|
138
|
+
// declaration needed): it sorts by link DEGREE — the both-directions
|
|
139
|
+
// raw row count — using the same directional-sum definition as the
|
|
140
|
+
// `linkCount` response field, so the sort key equals the field value for
|
|
141
|
+
// every note (self-loops included). See `queryNotes`/`getLinkCounts`.
|
|
118
142
|
orderBy?: string;
|
|
119
143
|
limit?: number;
|
|
120
144
|
offset?: number;
|
|
@@ -153,6 +177,8 @@ export interface NoteSummary {
|
|
|
153
177
|
createdAt: string;
|
|
154
178
|
updatedAt?: string;
|
|
155
179
|
tags?: string[];
|
|
180
|
+
/** Opt-in link degree (see `Note.linkCount`). */
|
|
181
|
+
linkCount?: number;
|
|
156
182
|
}
|
|
157
183
|
|
|
158
184
|
/**
|
|
@@ -169,6 +195,8 @@ export interface NoteIndex {
|
|
|
169
195
|
metadata?: Record<string, unknown>;
|
|
170
196
|
byteSize: number;
|
|
171
197
|
preview: string;
|
|
198
|
+
/** Opt-in link degree (see `Note.linkCount`). */
|
|
199
|
+
linkCount?: number;
|
|
172
200
|
}
|
|
173
201
|
|
|
174
202
|
/** Link with hydrated note summaries. */
|
|
@@ -313,7 +341,7 @@ export interface Store {
|
|
|
313
341
|
patch: {
|
|
314
342
|
description?: string | null;
|
|
315
343
|
fields?: Record<string, TagFieldSchema> | null;
|
|
316
|
-
relationships?:
|
|
344
|
+
relationships?: TagRelationshipMap | null;
|
|
317
345
|
parent_names?: string[] | null;
|
|
318
346
|
},
|
|
319
347
|
): Promise<TagRecord>;
|
package/package.json
CHANGED
package/src/auth.test.ts
CHANGED
|
@@ -26,7 +26,8 @@ import {
|
|
|
26
26
|
hashKey,
|
|
27
27
|
} from "./config.ts";
|
|
28
28
|
import { getVaultStore, clearVaultStoreCache } from "./vault-store.ts";
|
|
29
|
-
import { authenticateVaultRequest, authenticateGlobalRequest } from "./auth.ts";
|
|
29
|
+
import { authenticateVaultRequest, authenticateGlobalRequest, warnLegacyGlobalApiKeys } from "./auth.ts";
|
|
30
|
+
import type { StoredKey } from "./config.ts";
|
|
30
31
|
|
|
31
32
|
let tmpHome: string;
|
|
32
33
|
let prevHome: string | undefined;
|
|
@@ -442,3 +443,38 @@ describe("auth — VAULT_AUTH_TOKEN server-wide operator bearer", () => {
|
|
|
442
443
|
expect("error" in result).toBe(true);
|
|
443
444
|
});
|
|
444
445
|
});
|
|
446
|
+
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
// Legacy GLOBAL api_keys boot warning (security review — multi-user
|
|
449
|
+
// hardening). Cross-vault credentials in config.yaml must be surfaced loudly
|
|
450
|
+
// at boot, but never altered. Pure-function unit tests (no server boot).
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
describe("warnLegacyGlobalApiKeys (legacy cross-vault key boot warning)", () => {
|
|
453
|
+
function key(id: string): StoredKey {
|
|
454
|
+
return {
|
|
455
|
+
id,
|
|
456
|
+
label: id,
|
|
457
|
+
key_hash: `sha256:${id}`,
|
|
458
|
+
scope: "full",
|
|
459
|
+
created_at: new Date().toISOString(),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
test("warns when global api_keys are present", () => {
|
|
464
|
+
const msgs: string[] = [];
|
|
465
|
+
const count = warnLegacyGlobalApiKeys([key("a"), key("b")], (m) => msgs.push(m));
|
|
466
|
+
expect(count).toBe(2);
|
|
467
|
+
expect(msgs).toHaveLength(1);
|
|
468
|
+
expect(msgs[0]).toContain("legacy GLOBAL api_key");
|
|
469
|
+
expect(msgs[0]).toContain("CROSS-VAULT");
|
|
470
|
+
// Heads-up only — must signal it does NOT alter the keys.
|
|
471
|
+
expect(msgs[0]).toContain("remain active");
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("silent when there are no global api_keys", () => {
|
|
475
|
+
const msgs: string[] = [];
|
|
476
|
+
expect(warnLegacyGlobalApiKeys([], (m) => msgs.push(m))).toBe(0);
|
|
477
|
+
expect(warnLegacyGlobalApiKeys(undefined, (m) => msgs.push(m))).toBe(0);
|
|
478
|
+
expect(msgs).toHaveLength(0);
|
|
479
|
+
});
|
|
480
|
+
});
|
package/src/auth.ts
CHANGED
|
@@ -171,6 +171,35 @@ export function warnLegacyOnce(cacheKey: string, context: string): void {
|
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Boot-time warning for legacy GLOBAL `api_keys` in `config.yaml` (security
|
|
176
|
+
* review — multi-user hardening). Those keys are CROSS-VAULT credentials: a
|
|
177
|
+
* single key authenticates against EVERY vault on this server (see the global
|
|
178
|
+
* `api_keys` branch in `authenticate` ~L283). That predates per-vault keys +
|
|
179
|
+
* tag-scoped hub JWTs and is a confidentiality hazard once a server hosts
|
|
180
|
+
* multiple users' vaults — one user's global key reads another's vault.
|
|
181
|
+
*
|
|
182
|
+
* WARNING ONLY — never touches the keys (the operator owns them). The
|
|
183
|
+
* verification flagged 6 such keys on the live box; this surfaces them at
|
|
184
|
+
* boot so they're rotated/removed before multi-user sharing. Returns the
|
|
185
|
+
* count it warned about (0 = silent) so callers / tests can assert.
|
|
186
|
+
*/
|
|
187
|
+
export function warnLegacyGlobalApiKeys(
|
|
188
|
+
globalApiKeys: StoredKey[] | undefined,
|
|
189
|
+
warn: (msg: string) => void = console.warn,
|
|
190
|
+
): number {
|
|
191
|
+
const count = globalApiKeys?.length ?? 0;
|
|
192
|
+
if (count === 0) return 0;
|
|
193
|
+
warn(
|
|
194
|
+
`[auth] WARNING: ${count} legacy GLOBAL api_key(s) found in config.yaml. ` +
|
|
195
|
+
"These are CROSS-VAULT credentials (each grants access to every vault on this server) " +
|
|
196
|
+
"and predate per-vault keys + tag-scoped hub JWTs. Before multi-user sharing, ROTATE or " +
|
|
197
|
+
"REMOVE them — a global key leaks one user's vault to another. They remain active (the " +
|
|
198
|
+
"operator owns them); this is a heads-up, not an automatic change.",
|
|
199
|
+
);
|
|
200
|
+
return count;
|
|
201
|
+
}
|
|
202
|
+
|
|
174
203
|
/** Read-only tools (the only tools allowed for "read" permission). */
|
|
175
204
|
const READ_TOOLS = new Set([
|
|
176
205
|
"query-notes",
|