@chatpanel/events 0.102.0 → 0.103.1
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/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +1 -4
- package/pdf-layout.js +7 -253
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -442
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/vault.js
CHANGED
|
@@ -1,250 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// ChatPanel already encrypts two things at rest (meeting transcripts, stored API keys), and
|
|
4
|
-
// both of those modules say the same thing in their own comments: the key sits in storage
|
|
5
|
-
// beside the data, so it is obfuscation, not protection, and the real answer is "a key
|
|
6
|
-
// derived from a user passphrase and never written to disk". This is that answer.
|
|
7
|
-
//
|
|
8
|
-
// WHAT IS DIFFERENT HERE, and why it justifies a third crypto path rather than a fourth
|
|
9
|
-
// caller of the second one:
|
|
10
|
-
//
|
|
11
|
-
// • The key is derived from a passphrase and lives ONLY in session memory. Disk holds
|
|
12
|
-
// ciphertext, the KDF parameters, and a verifier — nothing that yields the key.
|
|
13
|
-
// • It FAILS CLOSED. The other two deliberately fail open: a crypto hiccup must never lose
|
|
14
|
-
// someone's API key, so they fall back to plaintext. A vault that quietly writes
|
|
15
|
-
// plaintext when something goes wrong is not a vault, so every failure here is an error
|
|
16
|
-
// the caller has to handle.
|
|
17
|
-
// • EVERYTHING is encrypted, titles included. A list of entry names is a list of the
|
|
18
|
-
// accounts someone has — "Chatpanel", "bank", "ex-employer" — which is most of what an
|
|
19
|
-
// attacker wanted. So a locked vault can say how many entries it holds and nothing else,
|
|
20
|
-
// and search runs over decrypted entries in memory, after unlocking.
|
|
21
|
-
//
|
|
22
|
-
// Pure and portable: the crypto is WebCrypto, which the browser, Node and a mobile runtime
|
|
23
|
-
// all have, and `subtle`/`random`/`now` are injected so this is testable and replayable. The
|
|
24
|
-
// envelope is versioned and self-describing — a vault written by the extension today must
|
|
25
|
-
// open in a phone app in two years, and the iteration count must be raisable without
|
|
26
|
-
// stranding anyone's data.
|
|
27
|
-
|
|
28
|
-
export class VaultError extends Error {
|
|
29
|
-
constructor(code, message) { super(message); this.name = 'VaultError'; this.code = code; }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const VAULT_VERSION = 1;
|
|
33
|
-
|
|
34
|
-
// PBKDF2-SHA256. Higher than the backup envelope's 250k because a vault is a standing
|
|
35
|
-
// target rather than a file someone chose to export, and unlocking is a once-per-session
|
|
36
|
-
// cost a person is already waiting through. The parameters travel WITH the vault, so this
|
|
37
|
-
// number can rise later without breaking anything already written.
|
|
38
|
-
export const KDF_ITERATIONS = 310_000;
|
|
39
|
-
export const KDF_HASH = 'SHA-256';
|
|
40
|
-
|
|
41
|
-
export const MAX_TITLE_CHARS = 200;
|
|
42
|
-
export const MAX_NOTE_CHARS = 20_000;
|
|
43
|
-
export const MAX_SECRET_CHARS = 8_000;
|
|
44
|
-
export const MAX_ENTRIES = 2_000;
|
|
45
|
-
|
|
46
|
-
// What the verifier proves: that the passphrase derives the same key as last time. It is a
|
|
47
|
-
// constant sealed under the key, so a wrong passphrase fails the AES-GCM tag check and is
|
|
48
|
-
// TOLD APART from an empty vault — "wrong passphrase" and "nothing here" must never look
|
|
49
|
-
// alike to a user staring at an empty list.
|
|
50
|
-
const VERIFIER_PLAINTEXT = 'chatpanel-vault-v1';
|
|
51
|
-
|
|
52
|
-
const enc = new TextEncoder();
|
|
53
|
-
const dec = new TextDecoder();
|
|
54
|
-
|
|
55
|
-
const B64_CHUNK = 0x8000;
|
|
56
|
-
export function toB64(bytes) {
|
|
57
|
-
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
58
|
-
let binary = '';
|
|
59
|
-
for (let i = 0; i < view.length; i += B64_CHUNK) binary += String.fromCharCode(...view.subarray(i, i + B64_CHUNK));
|
|
60
|
-
return btoa(binary);
|
|
61
|
-
}
|
|
62
|
-
export function fromB64(s) {
|
|
63
|
-
const binary = atob(String(s || ''));
|
|
64
|
-
const out = new Uint8Array(binary.length);
|
|
65
|
-
for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function subtleOf(subtle) {
|
|
70
|
-
const s = subtle || globalThis.crypto?.subtle;
|
|
71
|
-
if (!s) throw new VaultError('NO_CRYPTO', 'WebCrypto is unavailable in this runtime');
|
|
72
|
-
return s;
|
|
73
|
-
}
|
|
74
|
-
function randomOf(random) {
|
|
75
|
-
const r = random || ((n) => globalThis.crypto.getRandomValues(new Uint8Array(n)));
|
|
76
|
-
return r;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// Entries
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
const str = (v) => typeof v === 'string';
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* An entry is a title, an optional note, and an optional secret. The secret is not special
|
|
87
|
-
* to the crypto — everything is sealed together — it is special to the UI, which must not
|
|
88
|
-
* put it on screen without being asked.
|
|
89
|
-
*/
|
|
90
|
-
export function validateEntry(entry) {
|
|
91
|
-
if (!entry || typeof entry !== 'object') throw new VaultError('SHAPE', 'entry must be an object');
|
|
92
|
-
if (!str(entry.title) || !entry.title.trim()) throw new VaultError('SHAPE', 'entry.title required');
|
|
93
|
-
if (entry.title.length > MAX_TITLE_CHARS) throw new VaultError('SHAPE', `entry.title exceeds ${MAX_TITLE_CHARS} chars`);
|
|
94
|
-
if (entry.note != null && (!str(entry.note) || entry.note.length > MAX_NOTE_CHARS)) {
|
|
95
|
-
throw new VaultError('SHAPE', `entry.note must be a string under ${MAX_NOTE_CHARS} chars`);
|
|
96
|
-
}
|
|
97
|
-
if (entry.secret != null && (!str(entry.secret) || entry.secret.length > MAX_SECRET_CHARS)) {
|
|
98
|
-
throw new VaultError('SHAPE', `entry.secret must be a string under ${MAX_SECRET_CHARS} chars`);
|
|
99
|
-
}
|
|
100
|
-
return entry;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* What may be shown, logged or handed to a widget WITHOUT revealing anything.
|
|
105
|
-
*
|
|
106
|
-
* `hasSecret` rather than the secret, and never the note: a note is where people put the
|
|
107
|
-
* answers to their security questions. This is the shape every list, search result and log
|
|
108
|
-
* line uses, so there is one definition of "safe to show" instead of one per caller.
|
|
109
|
-
*/
|
|
110
|
-
export function entryMeta(entry) {
|
|
111
|
-
return {
|
|
112
|
-
id: entry.id,
|
|
113
|
-
title: entry.title,
|
|
114
|
-
hasSecret: !!entry.secret,
|
|
115
|
-
updatedAt: entry.updatedAt || 0,
|
|
116
|
-
createdAt: entry.createdAt || 0,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Search decrypted entries. Titles and notes match; secrets are never searched. */
|
|
121
|
-
export function searchEntries(entries, query) {
|
|
122
|
-
const q = String(query || '').trim().toLowerCase();
|
|
123
|
-
if (!q) return entries;
|
|
124
|
-
// Searching secret material would leak it through timing and through "1 result" on a page
|
|
125
|
-
// that never shows the secret — and nobody searches for a password they already have.
|
|
126
|
-
return entries.filter((e) => `${e.title || ''}\n${e.note || ''}`.toLowerCase().includes(q));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
// Keys, sealing, opening
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
|
|
133
|
-
export async function deriveKey(passphrase, salt, iterations = KDF_ITERATIONS, { subtle } = {}) {
|
|
134
|
-
const s = subtleOf(subtle);
|
|
135
|
-
if (!str(passphrase) || !passphrase) throw new VaultError('NO_PASSPHRASE', 'a passphrase is required');
|
|
136
|
-
const base = await s.importKey('raw', enc.encode(passphrase), 'PBKDF2', false, ['deriveKey']);
|
|
137
|
-
return s.deriveKey(
|
|
138
|
-
{ name: 'PBKDF2', salt, iterations, hash: KDF_HASH },
|
|
139
|
-
base,
|
|
140
|
-
{ name: 'AES-GCM', length: 256 },
|
|
141
|
-
// Extractable, because the host has to hand this key to session storage to survive the
|
|
142
|
-
// panel being closed and reopened. It never touches disk — see the client's session-only
|
|
143
|
-
// storage — and a non-extractable key would force a re-prompt on every panel open, which
|
|
144
|
-
// is the kind of friction that makes people choose a weaker passphrase.
|
|
145
|
-
true,
|
|
146
|
-
['encrypt', 'decrypt'],
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function seal(key, plaintextString, { subtle, random } = {}) {
|
|
151
|
-
const s = subtleOf(subtle);
|
|
152
|
-
const iv = randomOf(random)(12); // fresh per seal — an IV reused under one key breaks GCM
|
|
153
|
-
const ct = await s.encrypt({ name: 'AES-GCM', iv }, key, enc.encode(plaintextString));
|
|
154
|
-
return { iv: toB64(iv), ct: toB64(ct) };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function open(key, sealed, { subtle } = {}) {
|
|
158
|
-
const s = subtleOf(subtle);
|
|
159
|
-
if (!sealed?.iv || !sealed?.ct) throw new VaultError('CORRUPT', 'sealed value is missing its iv or ciphertext');
|
|
160
|
-
try {
|
|
161
|
-
const out = await s.decrypt({ name: 'AES-GCM', iv: fromB64(sealed.iv) }, key, fromB64(sealed.ct));
|
|
162
|
-
return dec.decode(out);
|
|
163
|
-
} catch {
|
|
164
|
-
// AES-GCM authenticates: this is either the wrong key or tampered bytes, and the caller
|
|
165
|
-
// cannot tell which. Both mean "do not trust what came back", which is the only thing
|
|
166
|
-
// the caller needs to act on.
|
|
167
|
-
throw new VaultError('BAD_KEY', 'wrong passphrase, or the data has been altered');
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** A brand-new, empty vault. Returns what is safe to write to disk. */
|
|
172
|
-
export async function createVault(passphrase, { subtle, random, now = () => Date.now() } = {}) {
|
|
173
|
-
const salt = randomOf(random)(16);
|
|
174
|
-
const key = await deriveKey(passphrase, salt, KDF_ITERATIONS, { subtle });
|
|
175
|
-
const verifier = await seal(key, VERIFIER_PLAINTEXT, { subtle, random });
|
|
176
|
-
return {
|
|
177
|
-
vault: {
|
|
178
|
-
version: VAULT_VERSION,
|
|
179
|
-
kdf: { name: 'PBKDF2', hash: KDF_HASH, iterations: KDF_ITERATIONS, salt: toB64(salt) },
|
|
180
|
-
verifier,
|
|
181
|
-
entries: {},
|
|
182
|
-
createdAt: now(),
|
|
183
|
-
},
|
|
184
|
-
key,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Derive the key and PROVE it is the right one before the caller stores it.
|
|
190
|
-
*
|
|
191
|
-
* Without the proof, a typo would be discovered later, one entry at a time, as
|
|
192
|
-
* "corrupt data" — and a user would reasonably conclude their vault was destroyed.
|
|
193
|
-
*/
|
|
194
|
-
export async function unlockVault(vault, passphrase, { subtle } = {}) {
|
|
195
|
-
if (!vault?.kdf?.salt) throw new VaultError('NO_VAULT', 'no vault has been created yet');
|
|
196
|
-
if (vault.version > VAULT_VERSION) {
|
|
197
|
-
// Forward-compat is a promise in one direction only. Opening a newer vault with older
|
|
198
|
-
// rules risks writing back something the newer client cannot read.
|
|
199
|
-
throw new VaultError('TOO_NEW', 'this vault was written by a newer version of ChatPanel');
|
|
200
|
-
}
|
|
201
|
-
const key = await deriveKey(passphrase, fromB64(vault.kdf.salt), vault.kdf.iterations || KDF_ITERATIONS, { subtle });
|
|
202
|
-
await open(key, vault.verifier, { subtle }); // throws BAD_KEY on a wrong passphrase
|
|
203
|
-
return key;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Seal one entry. The whole entry, titles included — see the note at the top. */
|
|
207
|
-
export async function sealEntry(key, entry, { subtle, random, now = () => Date.now() } = {}) {
|
|
208
|
-
validateEntry(entry);
|
|
209
|
-
const at = now();
|
|
210
|
-
const full = { ...entry, updatedAt: at, createdAt: entry.createdAt || at };
|
|
211
|
-
const sealed = await seal(key, JSON.stringify(full), { subtle, random });
|
|
212
|
-
// updatedAt is deliberately OUTSIDE the ciphertext as well: a list has to sort without
|
|
213
|
-
// unlocking, and "something changed at 14:02" is not a secret worth the cost of hiding.
|
|
214
|
-
return { id: entry.id, updatedAt: at, ...sealed };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export async function openEntry(key, record, { subtle } = {}) {
|
|
218
|
-
const json = await open(key, record, { subtle });
|
|
219
|
-
try {
|
|
220
|
-
const entry = JSON.parse(json);
|
|
221
|
-
return { ...entry, id: record.id ?? entry.id };
|
|
222
|
-
} catch {
|
|
223
|
-
throw new VaultError('CORRUPT', 'entry could not be read');
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
// Locking
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
|
|
231
|
-
export const DEFAULT_LOCK_MS = 15 * 60_000;
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Auto-lock is measured from the last USE, not from the unlock: a vault someone is actively
|
|
235
|
-
* working in should not lock under their hands, and one they walked away from should.
|
|
236
|
-
*/
|
|
237
|
-
export function isLocked({ unlockedAt = 0, lastUsedAt = 0, now = Date.now(), timeoutMs = DEFAULT_LOCK_MS } = {}) {
|
|
238
|
-
if (!unlockedAt) return true;
|
|
239
|
-
if (!timeoutMs) return false; // 0 = stay unlocked until the session ends or the user locks
|
|
240
|
-
return now - Math.max(unlockedAt, lastUsedAt) >= timeoutMs;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** How many entries a locked vault may admit to holding. A count is not a secret; names are. */
|
|
244
|
-
export function lockedSummary(vault) {
|
|
245
|
-
return { exists: !!vault?.kdf, entries: Object.keys(vault?.entries || {}).length, locked: true };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export function canAddEntry(vault) {
|
|
249
|
-
return Object.keys(vault?.entries || {}).length < MAX_ENTRIES;
|
|
250
|
-
}
|
|
1
|
+
class i extends Error{constructor(e,n){super(n),this.name="VaultError",this.code=e}}const w=1,s=31e4,h="SHA-256",p=200,A=2e4,E=8e3,m=2e3,C="chatpanel-vault-v1",x=new TextEncoder,v=new TextDecoder,y=32768;function d(t){const e=t instanceof Uint8Array?t:new Uint8Array(t);let n="";for(let r=0;r<e.length;r+=y)n+=String.fromCharCode(...e.subarray(r,r+y));return btoa(n)}function l(t){const e=atob(String(t||"")),n=new Uint8Array(e.length);for(let r=0;r<e.length;r+=1)n[r]=e.charCodeAt(r);return n}function f(t){const e=t||globalThis.crypto?.subtle;if(!e)throw new i("NO_CRYPTO","WebCrypto is unavailable in this runtime");return e}function S(t){return t||(n=>globalThis.crypto.getRandomValues(new Uint8Array(n)))}const u=t=>typeof t=="string";function O(t){if(!t||typeof t!="object")throw new i("SHAPE","entry must be an object");if(!u(t.title)||!t.title.trim())throw new i("SHAPE","entry.title required");if(t.title.length>p)throw new i("SHAPE",`entry.title exceeds ${p} chars`);if(t.note!=null&&(!u(t.note)||t.note.length>A))throw new i("SHAPE",`entry.note must be a string under ${A} chars`);if(t.secret!=null&&(!u(t.secret)||t.secret.length>E))throw new i("SHAPE",`entry.secret must be a string under ${E} chars`);return t}function P(t){return{id:t.id,title:t.title,hasSecret:!!t.secret,updatedAt:t.updatedAt||0,createdAt:t.createdAt||0}}function H(t,e){const n=String(e||"").trim().toLowerCase();return n?t.filter(r=>`${r.title||""}
|
|
2
|
+
${r.note||""}`.toLowerCase().includes(n)):t}async function _(t,e,n=s,{subtle:r}={}){const o=f(r);if(!u(t)||!t)throw new i("NO_PASSPHRASE","a passphrase is required");const a=await o.importKey("raw",x.encode(t),"PBKDF2",!1,["deriveKey"]);return o.deriveKey({name:"PBKDF2",salt:e,iterations:n,hash:h},a,{name:"AES-GCM",length:256},!0,["encrypt","decrypt"])}async function g(t,e,{subtle:n,random:r}={}){const o=f(n),a=S(r)(12),c=await o.encrypt({name:"AES-GCM",iv:a},t,x.encode(e));return{iv:d(a),ct:d(c)}}async function T(t,e,{subtle:n}={}){const r=f(n);if(!e?.iv||!e?.ct)throw new i("CORRUPT","sealed value is missing its iv or ciphertext");try{const o=await r.decrypt({name:"AES-GCM",iv:l(e.iv)},t,l(e.ct));return v.decode(o)}catch{throw new i("BAD_KEY","wrong passphrase, or the data has been altered")}}async function k(t,{subtle:e,random:n,now:r=()=>Date.now()}={}){const o=S(n)(16),a=await _(t,o,s,{subtle:e}),c=await g(a,C,{subtle:e,random:n});return{vault:{version:w,kdf:{name:"PBKDF2",hash:h,iterations:s,salt:d(o)},verifier:c,entries:{},createdAt:r()},key:a}}async function K(t,e,{subtle:n}={}){if(!t?.kdf?.salt)throw new i("NO_VAULT","no vault has been created yet");if(t.version>w)throw new i("TOO_NEW","this vault was written by a newer version of ChatPanel");const r=await _(e,l(t.kdf.salt),t.kdf.iterations||s,{subtle:n});return await T(r,t.verifier,{subtle:n}),r}async function N(t,e,{subtle:n,random:r,now:o=()=>Date.now()}={}){O(e);const a=o(),c={...e,updatedAt:a,createdAt:e.createdAt||a},b=await g(t,JSON.stringify(c),{subtle:n,random:r});return{id:e.id,updatedAt:a,...b}}async function D(t,e,{subtle:n}={}){const r=await T(t,e,{subtle:n});try{const o=JSON.parse(r);return{...o,id:e.id??o.id}}catch{throw new i("CORRUPT","entry could not be read")}}const R=15*6e4;function U({unlockedAt:t=0,lastUsedAt:e=0,now:n=Date.now(),timeoutMs:r=R}={}){return t?r?n-Math.max(t,e)>=r:!1:!0}function L(t){return{exists:!!t?.kdf,entries:Object.keys(t?.entries||{}).length,locked:!0}}function M(t){return Object.keys(t?.entries||{}).length<m}export{R as DEFAULT_LOCK_MS,h as KDF_HASH,s as KDF_ITERATIONS,m as MAX_ENTRIES,A as MAX_NOTE_CHARS,E as MAX_SECRET_CHARS,p as MAX_TITLE_CHARS,w as VAULT_VERSION,i as VaultError,M as canAddEntry,k as createVault,_ as deriveKey,P as entryMeta,l as fromB64,U as isLocked,L as lockedSummary,D as openEntry,N as sealEntry,H as searchEntries,d as toB64,K as unlockVault,O as validateEntry};
|
package/view.js
CHANGED
|
@@ -1,88 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// component instead of a paragraph of text.
|
|
3
|
-
//
|
|
4
|
-
// This is NOT the model writing HTML. That already exists (an ```html artifact runs in the
|
|
5
|
-
// sandbox) and is deliberately powerless: it is untrusted text, so it can only draw. A view
|
|
6
|
-
// is declared by a capability that a user or admin approved at load time, which is what
|
|
7
|
-
// makes it safe to give it something a model-authored page can never have — the ability to
|
|
8
|
-
// ACT, by invoking capabilities.
|
|
9
|
-
//
|
|
10
|
-
// The rule that keeps that safe is one line: a view may invoke only what its own capability
|
|
11
|
-
// is already allowed to invoke. A calculator view can compute because its capability may; it
|
|
12
|
-
// cannot read history unless that capability was granted `history` in the first place. There
|
|
13
|
-
// is no path here that widens a permission — `mayInvoke` can only ever name capabilities the
|
|
14
|
-
// declaring capability already lists in `requires`, and validateViewInvocation refuses
|
|
15
|
-
// anything outside that set before the kernel is ever consulted.
|
|
16
|
-
//
|
|
17
|
-
// Pure and host-free by design: no DOM, no postMessage, no chrome.*. The client owns the
|
|
18
|
-
// transport; this owns what is legal to say over it.
|
|
19
|
-
|
|
20
|
-
import { EventError } from './event.js';
|
|
21
|
-
|
|
22
|
-
const str = (v) => typeof v === 'string' && v.length > 0;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Validate a view DECLARATION. Like the rest of the capability contract this must be
|
|
26
|
-
* readable — and therefore approvable — without executing anything.
|
|
27
|
-
*/
|
|
28
|
-
export function validateView(view, capability = null) {
|
|
29
|
-
if (!view || typeof view !== 'object') throw new EventError('SHAPE', 'view must be an object');
|
|
30
|
-
if (!str(view.id)) throw new EventError('SHAPE', 'view.id required');
|
|
31
|
-
if (!str(view.html)) throw new EventError('SHAPE', 'view.html required — a self-contained document');
|
|
32
|
-
if (view.mayInvoke != null && !(Array.isArray(view.mayInvoke) && view.mayInvoke.every(str))) {
|
|
33
|
-
throw new EventError('SHAPE', 'view.mayInvoke must be string[]');
|
|
34
|
-
}
|
|
35
|
-
// NO PRIVILEGE ESCALATION BY DECLARATION. A view is part of its capability, so it cannot
|
|
36
|
-
// reach past it: every id it wants to call must already be in that capability's `requires`
|
|
37
|
-
// (or be the capability itself). Caught here, at approval time, rather than at call time.
|
|
38
|
-
if (capability && view.mayInvoke?.length) {
|
|
39
|
-
const allowed = new Set([capability.id, ...(capability.requires || [])]);
|
|
40
|
-
const extra = view.mayInvoke.filter((id) => !allowed.has(id));
|
|
41
|
-
if (extra.length) {
|
|
42
|
-
throw new EventError('CONTRADICTION',
|
|
43
|
-
`view.mayInvoke exceeds its capability: ${extra.join(', ')} not in requires`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (view.height != null && !(Number.isInteger(view.height) && view.height > 0 && view.height <= 2000)) {
|
|
47
|
-
throw new EventError('SHAPE', 'view.height must be a positive integer <= 2000');
|
|
48
|
-
}
|
|
49
|
-
return view;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Turn a message a VIEW sent into a capability invocation the kernel can judge — or refuse
|
|
54
|
-
* it. The view is sandboxed and its messages are untrusted input, so nothing here trusts a
|
|
55
|
-
* field: the capability id is checked against what the declaration allows, not against what
|
|
56
|
-
* the message claims to be entitled to.
|
|
57
|
-
*
|
|
58
|
-
* Returns { capability, args, callId }. Throws EventError otherwise.
|
|
59
|
-
*/
|
|
60
|
-
export function validateViewInvocation(msg, capability) {
|
|
61
|
-
if (!capability?.view) throw new EventError('SHAPE', 'capability declares no view');
|
|
62
|
-
if (!msg || typeof msg !== 'object') throw new EventError('SHAPE', 'view message must be an object');
|
|
63
|
-
if (!str(msg.callId)) throw new EventError('SHAPE', 'view message needs a callId to correlate its result');
|
|
64
|
-
if (!str(msg.capability)) throw new EventError('SHAPE', 'view message must name a capability');
|
|
65
|
-
|
|
66
|
-
const allowed = new Set([capability.id, ...(capability.view.mayInvoke || [])]);
|
|
67
|
-
if (!allowed.has(msg.capability)) {
|
|
68
|
-
// The important refusal. A compromised or buggy view asking for something else stops
|
|
69
|
-
// here, before any kernel guard has to have an opinion about it.
|
|
70
|
-
throw new EventError('DENIED',
|
|
71
|
-
`view of "${capability.id}" may not invoke "${msg.capability}"`);
|
|
72
|
-
}
|
|
73
|
-
if (msg.args != null && (typeof msg.args !== 'object' || Array.isArray(msg.args))) {
|
|
74
|
-
throw new EventError('SHAPE', 'view invocation args must be an object');
|
|
75
|
-
}
|
|
76
|
-
return { capability: msg.capability, args: msg.args || {}, callId: msg.callId };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* The state a view is mounted with, as it appears on a capability RESULT. Kept separate from
|
|
81
|
-
* the canonical value: `value` is what the capability computed and what everything else
|
|
82
|
-
* reasons about; `view`/`state` are only how it is shown. A host that cannot render views
|
|
83
|
-
* ignores these two fields and still has the whole answer.
|
|
84
|
-
*/
|
|
85
|
-
export function viewResult(value, capability, state = null) {
|
|
86
|
-
if (!capability?.view) return { value };
|
|
87
|
-
return { value, view: capability.view.id, state: state ?? value };
|
|
88
|
-
}
|
|
1
|
+
import{EventError as t}from"./event.js";const i=e=>typeof e=="string"&&e.length>0;function w(e,n=null){if(!e||typeof e!="object")throw new t("SHAPE","view must be an object");if(!i(e.id))throw new t("SHAPE","view.id required");if(!i(e.html))throw new t("SHAPE","view.html required — a self-contained document");if(e.mayInvoke!=null&&!(Array.isArray(e.mayInvoke)&&e.mayInvoke.every(i)))throw new t("SHAPE","view.mayInvoke must be string[]");if(n&&e.mayInvoke?.length){const r=new Set([n.id,...n.requires||[]]),o=e.mayInvoke.filter(a=>!r.has(a));if(o.length)throw new t("CONTRADICTION",`view.mayInvoke exceeds its capability: ${o.join(", ")} not in requires`)}if(e.height!=null&&!(Number.isInteger(e.height)&&e.height>0&&e.height<=2e3))throw new t("SHAPE","view.height must be a positive integer <= 2000");return e}function s(e,n){if(!n?.view)throw new t("SHAPE","capability declares no view");if(!e||typeof e!="object")throw new t("SHAPE","view message must be an object");if(!i(e.callId))throw new t("SHAPE","view message needs a callId to correlate its result");if(!i(e.capability))throw new t("SHAPE","view message must name a capability");if(!new Set([n.id,...n.view.mayInvoke||[]]).has(e.capability))throw new t("DENIED",`view of "${n.id}" may not invoke "${e.capability}"`);if(e.args!=null&&(typeof e.args!="object"||Array.isArray(e.args)))throw new t("SHAPE","view invocation args must be an object");return{capability:e.capability,args:e.args||{},callId:e.callId}}function h(e,n,r=null){return n?.view?{value:e,view:n.view.id,state:r??e}:{value:e}}export{w as validateView,s as validateViewInvocation,h as viewResult};
|