@parley-im/parley 0.0.0-stage → 0.1.0
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/README.md +134 -2
- package/bin/parley +15 -0
- package/dist/account.js +79 -0
- package/dist/agents/claude.js +134 -0
- package/dist/agents/codex.js +128 -0
- package/dist/agents/process.js +47 -0
- package/dist/agents/types.js +1 -0
- package/dist/api.js +180 -0
- package/dist/charter.js +57 -0
- package/dist/cli.js +272 -0
- package/dist/config.js +97 -0
- package/dist/control.js +72 -0
- package/dist/daemon.js +42 -0
- package/dist/db.js +243 -0
- package/dist/mcp.js +153 -0
- package/dist/models.js +10 -0
- package/dist/relay.js +304 -0
- package/dist/runner.js +198 -0
- package/dist/seal.js +51 -0
- package/dist/server.js +308 -0
- package/dist/state.js +41 -0
- package/dist/transcript.js +53 -0
- package/package.json +46 -4
- package/web/app.css +404 -0
- package/web/chat.html +81 -0
- package/web/chat.js +901 -0
- package/web/ended.html +20 -0
- package/web/home.js +552 -0
- package/web/icon-180.png +0 -0
- package/web/icon.png +0 -0
- package/web/icon.svg +1 -0
- package/web/markdown.js +87 -0
- package/web/missing.html +20 -0
- package/web/panel.html +76 -0
- package/web/panel.js +224 -0
- package/web/seal.js +44 -0
package/web/panel.html
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Control panel · parley</title>
|
|
7
|
+
<link rel="icon" href="/static/icon.svg">
|
|
8
|
+
<link rel="stylesheet" href="/static/app.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body class="panel">
|
|
11
|
+
<header class="topbar">
|
|
12
|
+
<span class="mark" aria-hidden="true"><svg viewBox="4 4 24 24"><g fill="currentColor" stroke="none"><circle cx="8" cy="10" r="2.2"/><rect x="12" y="8" width="12" height="4" rx="2"/><rect x="6" y="14" width="20" height="4" rx="2" opacity=".7"/><rect x="6" y="20" width="13" height="4" rx="2" opacity=".7"/></g></svg></span>
|
|
13
|
+
<nav class="crumbs"><span class="product">parley</span><span class="slash">/</span><span class="here">Control panel</span></nav>
|
|
14
|
+
<span class="status" id="server"><span class="dot" id="server-dot"></span><span id="server-text">Checking…</span></span>
|
|
15
|
+
</header>
|
|
16
|
+
|
|
17
|
+
<div class="page">
|
|
18
|
+
<p class="notice" id="signed-out" hidden style="margin:18px 0 0">This computer isn't signed in to parley.im, so links only work on this computer. Run <code>parley login</code> in a terminal to share them.</p>
|
|
19
|
+
<div class="stats" id="stats"></div>
|
|
20
|
+
|
|
21
|
+
<div class="toolbar">
|
|
22
|
+
<div class="tabs" id="tabs" role="tablist"></div>
|
|
23
|
+
<input class="search" id="search" type="search" placeholder="Filter by tester or project" aria-label="Filter sessions">
|
|
24
|
+
<button class="btn primary" id="new" type="button">
|
|
25
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>New session
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="table" id="table"></div>
|
|
30
|
+
<p class="cell-muted" style="margin-top:12px">Only visible on this computer. Spend is what replies would cost at API prices; a Claude or ChatGPT subscription isn't billed per reply, but replies count toward its usage limits.</p>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="drawer-back" id="drawer-back" hidden></div>
|
|
34
|
+
<aside class="drawer" id="drawer" hidden aria-label="New session">
|
|
35
|
+
<header><h2>New session</h2><button class="btn ghost" id="close" type="button">Close</button></header>
|
|
36
|
+
<form id="form">
|
|
37
|
+
<div class="field">
|
|
38
|
+
<label for="f-dir">Project folder</label>
|
|
39
|
+
<input id="f-dir" name="projectDir" list="dirs" placeholder="/Users/you/src/app" required autocomplete="off">
|
|
40
|
+
<datalist id="dirs"></datalist>
|
|
41
|
+
<span class="help">The interviewer runs here, read-only.</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="field">
|
|
44
|
+
<label for="f-tester">Who it's for</label>
|
|
45
|
+
<input id="f-tester" name="tester" placeholder="Sam" required autocomplete="off">
|
|
46
|
+
</div>
|
|
47
|
+
<div class="field">
|
|
48
|
+
<label for="f-brief">Brief</label>
|
|
49
|
+
<textarea id="f-brief" name="brief" required placeholder="What the project is, how they'll try it, what you most want to learn, known issues to skip."></textarea>
|
|
50
|
+
<span class="help">Written to the interviewer. Your Claude or Codex can write a better one from inside the project; just ask it to make a parley.</span>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="field">
|
|
53
|
+
<label for="f-greeting">First message (optional)</label>
|
|
54
|
+
<input id="f-greeting" name="greeting" placeholder="Hi Sam! Thanks for trying it…" autocomplete="off">
|
|
55
|
+
</div>
|
|
56
|
+
<div class="field">
|
|
57
|
+
<label>Interviewer</label>
|
|
58
|
+
<div class="segmented">
|
|
59
|
+
<label><input type="radio" name="agent" value="claude" checked>Claude</label>
|
|
60
|
+
<label><input type="radio" name="agent" value="codex">Codex</label>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="field">
|
|
64
|
+
<label for="f-limit">Reply limit</label>
|
|
65
|
+
<input id="f-limit" name="replyLimit" type="number" min="1" style="width:120px">
|
|
66
|
+
<span class="help">The session pauses after this many replies until you add more.</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="form-error" id="form-error" hidden></div>
|
|
69
|
+
<div class="made" id="made" hidden></div>
|
|
70
|
+
<div><button class="btn primary" id="create" type="submit">Create session</button></div>
|
|
71
|
+
</form>
|
|
72
|
+
</aside>
|
|
73
|
+
|
|
74
|
+
<script src="/static/panel.js"></script>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
package/web/panel.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const $ = (id) => document.getElementById(id);
|
|
3
|
+
const el = (tag, props = {}, ...children) => {
|
|
4
|
+
const node = Object.assign(document.createElement(tag), props);
|
|
5
|
+
node.append(...children.filter((c) => c != null));
|
|
6
|
+
return node;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
let data = null;
|
|
10
|
+
let tab = "open";
|
|
11
|
+
let query = "";
|
|
12
|
+
// Inline confirmations survive the refresh that redraws the table.
|
|
13
|
+
const confirming = new Map();
|
|
14
|
+
|
|
15
|
+
const ago = (iso) => {
|
|
16
|
+
if (!iso) return "never";
|
|
17
|
+
const s = (Date.now() - Date.parse(iso)) / 1000;
|
|
18
|
+
if (s < 60) return "just now";
|
|
19
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
20
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
|
|
21
|
+
return new Date(iso).toLocaleDateString([], { month: "short", day: "numeric" });
|
|
22
|
+
};
|
|
23
|
+
const money = (usd) => `$${usd.toFixed(2)}`;
|
|
24
|
+
const today = (iso) => iso && new Date(iso).toDateString() === new Date().toDateString();
|
|
25
|
+
|
|
26
|
+
async function post(path, body) {
|
|
27
|
+
const res = await fetch(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body ?? {}) });
|
|
28
|
+
const json = await res.json().catch(() => ({}));
|
|
29
|
+
if (!res.ok) throw new Error(json.message || "That didn't work.");
|
|
30
|
+
return json;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function refresh() {
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch("/api/panel", { cache: "no-store" });
|
|
36
|
+
data = await res.json();
|
|
37
|
+
render();
|
|
38
|
+
} catch {
|
|
39
|
+
$("server-dot").className = "dot off";
|
|
40
|
+
$("server-text").textContent = "Server not responding";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function render() {
|
|
45
|
+
const { server, sessions, relay } = data;
|
|
46
|
+
// This computer's connection to parley.im, which shared links depend on.
|
|
47
|
+
const host = relay.relayUrl.replace(/^https?:\/\//, "");
|
|
48
|
+
const RELAY = {
|
|
49
|
+
connected: ["on", `${host} · ${relay.email}`],
|
|
50
|
+
connecting: ["busy", `Connecting to ${host}`],
|
|
51
|
+
offline: ["warn", `Reconnecting to ${host}`],
|
|
52
|
+
"signed-out": ["warn", "Not signed in · run parley login"],
|
|
53
|
+
unlinked: ["off", "Unlinked · run parley login"],
|
|
54
|
+
replaced: ["off", "Another parley took over"],
|
|
55
|
+
};
|
|
56
|
+
const [dot, label] = RELAY[relay.state] ?? ["warn", relay.state];
|
|
57
|
+
$("server-dot").className = `dot ${dot}`;
|
|
58
|
+
$("server-text").textContent = label;
|
|
59
|
+
$("server").title = relay.error ?? `Server up since ${new Date(server.startedAt).toLocaleString()}`;
|
|
60
|
+
$("signed-out").hidden = relay.state !== "signed-out" && relay.state !== "unlinked";
|
|
61
|
+
|
|
62
|
+
const open = sessions.filter((s) => s.status === "open");
|
|
63
|
+
const paused = open.filter((s) => s.repliesLeft === 0);
|
|
64
|
+
const unread = sessions.reduce((n, s) => n + s.stats.unread, 0);
|
|
65
|
+
const replies = sessions.reduce((n, s) => n + s.stats.replies, 0);
|
|
66
|
+
const spend = sessions.reduce((n, s) => n + s.stats.cost_usd, 0);
|
|
67
|
+
const busy = open.filter((s) => s.busy).length;
|
|
68
|
+
const stat = (label, value, note) => el("div", { className: "stat" }, el("div", { className: "label", textContent: label }), el("div", { className: "value", textContent: value }), el("div", { className: "note", textContent: note }));
|
|
69
|
+
$("stats").replaceChildren(
|
|
70
|
+
stat("Open sessions", String(open.length), paused.length ? `${paused.length} paused, out of replies` : busy ? `${busy} replying now` : "links are live"),
|
|
71
|
+
stat("New messages", String(unread), unread ? `in ${sessions.filter((s) => s.stats.unread).length} session(s)` : "you're caught up"),
|
|
72
|
+
stat("Replies", String(replies), `${sessions.filter((s) => today(s.stats.last_message_at)).length} session(s) active today`),
|
|
73
|
+
stat("Spend", money(spend), "at API prices, all sessions"),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const ended = sessions.filter((s) => s.status !== "open");
|
|
77
|
+
const tabs = [["open", "Open", open.length], ["ended", "Ended", ended.length], ["all", "All", sessions.length]];
|
|
78
|
+
$("tabs").replaceChildren(
|
|
79
|
+
...tabs.map(([key, label, count]) => {
|
|
80
|
+
const b = el("button", { type: "button", className: tab === key ? "on" : "", textContent: `${label} ${count}` });
|
|
81
|
+
b.onclick = () => ((tab = key), render());
|
|
82
|
+
return b;
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const q = query.toLowerCase();
|
|
87
|
+
const shown = sessions
|
|
88
|
+
.filter((s) => (tab === "all" ? true : tab === "open" ? s.status === "open" : s.status !== "open"))
|
|
89
|
+
.filter((s) => !q || `${s.tester} ${s.project} ${s.projectDir}`.toLowerCase().includes(q));
|
|
90
|
+
|
|
91
|
+
const head = el(
|
|
92
|
+
"div",
|
|
93
|
+
{ className: "thead label" },
|
|
94
|
+
el("span", { textContent: "Session" }),
|
|
95
|
+
el("span", { textContent: "Activity" }),
|
|
96
|
+
el("span", { textContent: "Messages" }),
|
|
97
|
+
el("span", { textContent: "Replies" }),
|
|
98
|
+
el("span", { textContent: "Spend" }),
|
|
99
|
+
el("span", { textContent: "" }),
|
|
100
|
+
);
|
|
101
|
+
if (!shown.length) {
|
|
102
|
+
const msg = sessions.length
|
|
103
|
+
? el("div", { className: "empty", textContent: q ? "Nothing matches that." : tab === "open" ? "Nothing open right now." : "Nothing here." })
|
|
104
|
+
: el("div", { className: "empty" }, "No sessions yet. Ask Claude or Codex in any project to make a parley for someone, or use New session.");
|
|
105
|
+
$("table").replaceChildren(msg);
|
|
106
|
+
} else $("table").replaceChildren(head, ...shown.map(row));
|
|
107
|
+
|
|
108
|
+
const dirs = [...new Set(sessions.map((s) => s.projectDir))];
|
|
109
|
+
$("dirs").replaceChildren(...dirs.map((d) => el("option", { value: d })));
|
|
110
|
+
if (!$("f-limit").value) $("f-limit").value = String(server.replyLimit);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function row(s) {
|
|
114
|
+
const open = s.status === "open";
|
|
115
|
+
const used = s.replyLimit - s.repliesLeft;
|
|
116
|
+
const name = el("div", { className: "name" }, el("span", { className: `dot ${!open ? "" : s.busy ? "busy" : s.repliesLeft === 0 ? "warn" : "on"}` }), s.tester);
|
|
117
|
+
if (s.stats.unread) name.append(el("span", { className: "badge", textContent: `${s.stats.unread} new` }));
|
|
118
|
+
const who = el("a", { className: "who-cell", href: `/o/${s.id}`, style: "text-decoration:none" }, name, el("div", { className: "where", textContent: `${s.project} · ${s.model ?? (s.agent === "codex" ? "Codex" : "Claude")} · ${s.id}`, title: s.projectDir }));
|
|
119
|
+
|
|
120
|
+
const activity = el(
|
|
121
|
+
"div",
|
|
122
|
+
{ className: "c-activity cell-muted" },
|
|
123
|
+
!open ? `ended ${ago(s.endedAt)}` : s.busy ? "replying now" : s.stats.last_tester_at ? `heard from ${ago(s.stats.last_tester_at)}` : "hasn't said anything yet",
|
|
124
|
+
);
|
|
125
|
+
const messages = el("div", { className: "c-messages num", textContent: `${s.stats.tester_messages} message${s.stats.tester_messages === 1 ? "" : "s"}` });
|
|
126
|
+
const pct = Math.min(100, Math.round((used / s.replyLimit) * 100));
|
|
127
|
+
const replies = el(
|
|
128
|
+
"div",
|
|
129
|
+
{ className: "c-replies" },
|
|
130
|
+
el("div", { className: "num", textContent: `${used} / ${s.replyLimit} replies` }),
|
|
131
|
+
el("div", { className: `meter ${s.repliesLeft === 0 ? "full" : ""}`, style: "margin:4px 0 0;max-width:140px" }, el("span", { style: `width:${pct}%` })),
|
|
132
|
+
);
|
|
133
|
+
const spend = el("div", { className: "c-spend num", textContent: s.stats.cost_usd ? money(s.stats.cost_usd) : s.agent === "codex" ? "n/a" : "$0.00" });
|
|
134
|
+
|
|
135
|
+
const actions = el("div", { className: "row-actions" });
|
|
136
|
+
if (open) {
|
|
137
|
+
if (s.repliesLeft <= 10) {
|
|
138
|
+
const more = el("button", { className: "btn", type: "button", textContent: "+50 replies" });
|
|
139
|
+
more.onclick = () => post(`/api/sessions/${s.id}/limit`, { limit: s.replyLimit + 50 }).then(refresh);
|
|
140
|
+
actions.append(more);
|
|
141
|
+
}
|
|
142
|
+
const copy = el("button", { className: "btn", type: "button", textContent: "Copy link" });
|
|
143
|
+
copy.onclick = async () => {
|
|
144
|
+
await navigator.clipboard.writeText(s.link);
|
|
145
|
+
copy.textContent = "Copied";
|
|
146
|
+
setTimeout(() => (copy.textContent = "Copy link"), 1500);
|
|
147
|
+
};
|
|
148
|
+
actions.append(copy);
|
|
149
|
+
}
|
|
150
|
+
actions.append(el("a", { className: "btn", href: `/o/${s.id}`, textContent: "Open" }));
|
|
151
|
+
actions.append(confirmButton(s.id, open ? "End" : "Delete", open ? "End for good?" : "Erase it?", () => post(`/api/sessions/${s.id}/${open ? "end" : "delete"}`)));
|
|
152
|
+
|
|
153
|
+
return el("div", { className: `trow ${open ? "" : "ended"}` }, who, activity, messages, replies, spend, actions);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Asks once more in place, instead of a browser dialog.
|
|
157
|
+
function confirmButton(id, label, question, action) {
|
|
158
|
+
const asking = (confirming.get(id) ?? 0) > Date.now();
|
|
159
|
+
const button = el("button", { className: asking ? "btn confirm" : "btn danger", type: "button", textContent: asking ? question : label });
|
|
160
|
+
button.onclick = async () => {
|
|
161
|
+
if (!((confirming.get(id) ?? 0) > Date.now())) {
|
|
162
|
+
confirming.set(id, Date.now() + 4000);
|
|
163
|
+
render();
|
|
164
|
+
setTimeout(render, 4100);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
confirming.delete(id);
|
|
168
|
+
button.disabled = true;
|
|
169
|
+
await action().catch((e) => alertInline(e.message));
|
|
170
|
+
refresh();
|
|
171
|
+
};
|
|
172
|
+
return button;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function alertInline(message) {
|
|
176
|
+
$("server-text").textContent = message;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
$("search").addEventListener("input", (e) => {
|
|
180
|
+
query = e.target.value;
|
|
181
|
+
render();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ---------- new session ----------
|
|
185
|
+
|
|
186
|
+
const openDrawer = () => {
|
|
187
|
+
$("drawer").hidden = $("drawer-back").hidden = false;
|
|
188
|
+
$("made").hidden = $("form-error").hidden = true;
|
|
189
|
+
$("f-dir").focus();
|
|
190
|
+
};
|
|
191
|
+
const closeDrawer = () => ($("drawer").hidden = $("drawer-back").hidden = true);
|
|
192
|
+
$("new").onclick = openDrawer;
|
|
193
|
+
$("close").onclick = closeDrawer;
|
|
194
|
+
$("drawer-back").onclick = closeDrawer;
|
|
195
|
+
addEventListener("keydown", (e) => e.key === "Escape" && closeDrawer());
|
|
196
|
+
|
|
197
|
+
$("form").addEventListener("submit", async (e) => {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
const form = new FormData($("form"));
|
|
200
|
+
$("form-error").hidden = true;
|
|
201
|
+
$("create").disabled = true;
|
|
202
|
+
try {
|
|
203
|
+
const s = await post("/api/sessions", Object.fromEntries(form));
|
|
204
|
+
const link = el("input", { value: s.link, readOnly: true, className: "search", style: "font-family:var(--mono);font-size:12px" });
|
|
205
|
+
const copy = el("button", { className: "btn primary", type: "button", textContent: "Copy link" });
|
|
206
|
+
copy.onclick = async () => {
|
|
207
|
+
await navigator.clipboard.writeText(s.link);
|
|
208
|
+
copy.textContent = "Copied";
|
|
209
|
+
};
|
|
210
|
+
$("made").replaceChildren(el("strong", { textContent: `Session for ${s.tester} is ready.` }), el("div", { className: "linkbox" }, link, copy), el("a", { className: "cell-muted", href: `/o/${s.id}`, textContent: "Open it" }));
|
|
211
|
+
$("made").hidden = false;
|
|
212
|
+
$("f-tester").value = $("f-brief").value = $("f-greeting").value = "";
|
|
213
|
+
refresh();
|
|
214
|
+
} catch (error) {
|
|
215
|
+
$("form-error").textContent = error.message;
|
|
216
|
+
$("form-error").hidden = false;
|
|
217
|
+
} finally {
|
|
218
|
+
$("create").disabled = false;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
refresh();
|
|
223
|
+
setInterval(refresh, 4000);
|
|
224
|
+
})();
|
package/web/seal.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The browser side of parley's end-to-end encryption (see src/seal.ts for the
|
|
2
|
+
// format). The key comes from the link after the "#" and never leaves this
|
|
3
|
+
// page except to encrypt and decrypt.
|
|
4
|
+
(function () {
|
|
5
|
+
const subtle = crypto.subtle;
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const decoder = new TextDecoder();
|
|
8
|
+
|
|
9
|
+
const toB64 = (bytes) => {
|
|
10
|
+
let s = "";
|
|
11
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
12
|
+
return btoa(s).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
13
|
+
};
|
|
14
|
+
const fromB64 = (text) => {
|
|
15
|
+
const bin = atob(text.replaceAll("-", "+").replaceAll("_", "/"));
|
|
16
|
+
return Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
17
|
+
};
|
|
18
|
+
const aad = (session, purpose) => encoder.encode(`parley:${session}:${purpose}`);
|
|
19
|
+
|
|
20
|
+
async function importKey(text) {
|
|
21
|
+
const raw = fromB64(text);
|
|
22
|
+
if (raw.length !== 32) throw new Error("bad key");
|
|
23
|
+
return subtle.importKey("raw", raw, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function sealBytes(key, session, purpose, plain) {
|
|
27
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
28
|
+
const sealed = new Uint8Array(await subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad(session, purpose) }, key, plain));
|
|
29
|
+
const out = new Uint8Array(12 + sealed.length);
|
|
30
|
+
out.set(iv);
|
|
31
|
+
out.set(sealed, 12);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function openBytes(key, session, purpose, data) {
|
|
36
|
+
const plain = await subtle.decrypt({ name: "AES-GCM", iv: data.subarray(0, 12), additionalData: aad(session, purpose) }, key, data.subarray(12));
|
|
37
|
+
return new Uint8Array(plain);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const seal = async (key, session, purpose, value) => toB64(await sealBytes(key, session, purpose, encoder.encode(JSON.stringify(value))));
|
|
41
|
+
const open = async (key, session, purpose, text) => JSON.parse(decoder.decode(await openBytes(key, session, purpose, fromB64(text))));
|
|
42
|
+
|
|
43
|
+
(typeof window !== "undefined" ? window : globalThis).ParleySeal = { importKey, seal, open, sealBytes, openBytes };
|
|
44
|
+
})();
|