@openparachute/vault 0.7.0-rc.7 → 0.7.0-rc.9

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/src/tag-scope.ts CHANGED
@@ -83,6 +83,66 @@ export function filterNotesByTagScope<T extends Note>(
83
83
  return notes.filter((n) => noteWithinTagScope(n, allowed, rawRoots));
84
84
  }
85
85
 
86
+ /**
87
+ * Is a SINGLE tag name visible to this token? The per-tag core of
88
+ * `noteWithinTagScope` — exact allowlist membership OR string-form root
89
+ * match — pulled out so the `validation_status` scrub below (which reasons
90
+ * about a warning's `schema`/`loser_schema` tag NAMES, not a note's whole
91
+ * tag set) uses the identical visibility rule. `rawRoots === null` (unscoped)
92
+ * → always visible.
93
+ */
94
+ function tagVisibleInScope(
95
+ tag: string,
96
+ allowed: Set<string> | null,
97
+ rawRoots: string[] | null,
98
+ ): boolean {
99
+ if (rawRoots === null) return true;
100
+ if (allowed && allowed.has(tag)) return true;
101
+ const root = tag.split("/")[0];
102
+ return !!root && rawRoots.includes(root);
103
+ }
104
+
105
+ /**
106
+ * Scrub a note's `validation_status` so a tag-scoped caller can't learn the
107
+ * SCHEMA SHAPE (field name, type, enum values) of an OUT-OF-SCOPE tag that
108
+ * happens to co-tag a note it can otherwise see (vault#555 auth review;
109
+ * the #560 leak class). Repro: a scoped caller reading a note tagged both
110
+ * `mine` (in scope) and `project-manhattan` (out of scope) received
111
+ * `validation_status.warnings: [{ schema: "project-manhattan", message:
112
+ * "'codeword' must be one of [fizzbuzz] ...", ... }]` and
113
+ * `schemas: ["project-manhattan"]` — leaking that tag's field/enum.
114
+ *
115
+ * Core produces the FULL status (scope-unaware by architecture, same as
116
+ * every other core surface); this server-layer scrub — mirroring
117
+ * `scrubTagFieldViolationsByScope` — drops any warning whose declaring
118
+ * tag (`schema`, and for a `schema_conflict` the overridden `loser_schema`)
119
+ * is out of scope, and filters the `schemas` array to visible tags. When
120
+ * nothing in-scope remains (the note's only schema-declaring tag was
121
+ * out-of-scope), returns `undefined` so the caller omits `validation_status`
122
+ * entirely — byte-identical to a note with no applicable schema. Unscoped
123
+ * callers (`rawRoots === null`) get the status untouched.
124
+ *
125
+ * Applied at every point a scoped caller receives `validation_status`: the
126
+ * MCP `query-notes` wrapper and the REST `GET /notes[/{id}]` read paths.
127
+ */
128
+ export function scrubValidationStatusByScope<
129
+ S extends { schemas: string[]; warnings: Array<{ schema: string; loser_schema?: string }> },
130
+ >(
131
+ status: S | null | undefined,
132
+ allowed: Set<string> | null,
133
+ rawRoots: string[] | null,
134
+ ): S | undefined {
135
+ if (!status || rawRoots === null) return status ?? undefined;
136
+ const schemas = status.schemas.filter((s) => tagVisibleInScope(s, allowed, rawRoots));
137
+ const warnings = status.warnings.filter(
138
+ (w) =>
139
+ tagVisibleInScope(w.schema, allowed, rawRoots) &&
140
+ (w.loser_schema === undefined || tagVisibleInScope(w.loser_schema, allowed, rawRoots)),
141
+ );
142
+ if (schemas.length === 0 && warnings.length === 0) return undefined;
143
+ return { ...status, schemas, warnings };
144
+ }
145
+
86
146
  /**
87
147
  * For write paths: a note being created/updated must end up carrying at
88
148
  * least one tag inside the allowlist. `tags` is the post-write tag set