@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/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?: Record<string, TagRelationship> | null;
344
+ relationships?: TagRelationshipMap | null;
317
345
  parent_names?: string[] | null;
318
346
  },
319
347
  ): Promise<TagRecord>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/vault",
3
- "version": "0.5.1",
3
+ "version": "0.5.2-rc.2",
4
4
  "description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
5
5
  "module": "src/cli.ts",
6
6
  "type": "module",
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",