@yadsh/dsh-qa-surface 0.7.3 → 0.8.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 +56 -9
- package/lib/access/model.js +24 -4
- package/lib/access/model.js.map +1 -1
- package/lib/access/role-repository.js +199 -91
- package/lib/access/role-repository.js.map +1 -1
- package/lib/access/service.js +194 -6
- package/lib/access/service.js.map +1 -1
- package/lib/account-remotes.js +2 -4
- package/lib/account-remotes.js.map +1 -1
- package/lib/accounts/database.js +447 -0
- package/lib/accounts/database.js.map +1 -0
- package/lib/accounts/file.js +27 -90
- package/lib/accounts/file.js.map +1 -1
- package/lib/accounts/store.js +210 -59
- package/lib/accounts/store.js.map +1 -1
- package/lib/admin/quality-store.js +199 -54
- package/lib/admin/quality-store.js.map +1 -1
- package/lib/admin/service.js +39 -6
- package/lib/admin/service.js.map +1 -1
- package/lib/cli.js +2 -1
- package/lib/cli.js.map +1 -1
- package/lib/client/QaSessionController.js +20 -2
- package/lib/client/QaSessionController.js.map +1 -1
- package/lib/client/QaSurface.js +69 -31
- package/lib/client/QaSurface.js.map +1 -1
- package/lib/client/QaTranscriptAdapter.js +4 -0
- package/lib/client/QaTranscriptAdapter.js.map +1 -1
- package/lib/client/admin/QaAdmin.js +17 -7
- package/lib/client/admin/QaAdmin.js.map +1 -1
- package/lib/client/admin/{format.js → copy.js} +1 -1
- package/lib/client/admin/copy.js.map +1 -0
- package/lib/client/admin/pages/Conversations.js +1 -1
- package/lib/client/admin/pages/Conversations.js.map +1 -1
- package/lib/client/admin/pages/Overview.js +1 -1
- package/lib/client/admin/pages/Overview.js.map +1 -1
- package/lib/client/admin/pages/Quality.js +1 -1
- package/lib/client/admin/pages/Quality.js.map +1 -1
- package/lib/client/admin/pages/Review.js +1 -1
- package/lib/client/admin/pages/Review.js.map +1 -1
- package/lib/client/admin/pages/Users.js +6 -2
- package/lib/client/admin/pages/Users.js.map +1 -1
- package/lib/client/admin/shared.js +1 -1
- package/lib/client/admin/shared.js.map +1 -1
- package/lib/client/components/Markdown.js +14 -206
- package/lib/client/components/Markdown.js.map +1 -1
- package/lib/client/components/QaChangelog.js +60 -0
- package/lib/client/components/QaChangelog.js.map +1 -1
- package/lib/client/components/QaHeader.js +1 -3
- package/lib/client/components/QaHeader.js.map +1 -1
- package/lib/client/components/QaMessage.js +9 -4
- package/lib/client/components/QaMessage.js.map +1 -1
- package/lib/client/components/QaSidebar.js +5 -2
- package/lib/client/components/QaSidebar.js.map +1 -1
- package/lib/client/components/QaSourcesPanel.js +3 -18
- package/lib/client/components/QaSourcesPanel.js.map +1 -1
- package/lib/client/components/QaWorkGroup.js +4 -8
- package/lib/client/components/QaWorkGroup.js.map +1 -1
- package/lib/client/components/source-icon.js +26 -0
- package/lib/client/components/source-icon.js.map +1 -0
- package/lib/client/components/thinking-phrases.js +2 -11
- package/lib/client/components/thinking-phrases.js.map +1 -1
- package/lib/client/components/use-now.js +21 -0
- package/lib/client/components/use-now.js.map +1 -0
- package/lib/client/index.js +11 -3
- package/lib/client/index.js.map +1 -1
- package/lib/client/lineage.js +18 -0
- package/lib/client/lineage.js.map +1 -0
- package/lib/client/markdown/CodeBlock.js +28 -0
- package/lib/client/markdown/CodeBlock.js.map +1 -0
- package/lib/client/markdown/blocks.js +342 -0
- package/lib/client/markdown/blocks.js.map +1 -0
- package/lib/client/markdown/highlight.js +1089 -0
- package/lib/client/markdown/highlight.js.map +1 -0
- package/lib/client/markdown/inline.js +377 -0
- package/lib/client/markdown/inline.js.map +1 -0
- package/lib/client/markdown/render.js +147 -0
- package/lib/client/markdown/render.js.map +1 -0
- package/lib/client/markdown/source-chip.js +55 -0
- package/lib/client/markdown/source-chip.js.map +1 -0
- package/lib/client/markdown/use-transient-flag.js +27 -0
- package/lib/client/markdown/use-transient-flag.js.map +1 -0
- package/lib/client/role/RoleSelector.js +11 -2
- package/lib/client/role/RoleSelector.js.map +1 -1
- package/lib/client/role/preview.js +123 -0
- package/lib/client/role/preview.js.map +1 -0
- package/lib/client/styles.js +73 -19
- package/lib/client/styles.js.map +1 -1
- package/lib/client.js +3137 -761
- package/lib/client.js.map +1 -1
- package/lib/config-resolvers/accounts.js +15 -0
- package/lib/config-resolvers/accounts.js.map +1 -1
- package/lib/config-resolvers/defaults.js +8 -0
- package/lib/config-resolvers/defaults.js.map +1 -1
- package/lib/config-resolvers/sources.js +16 -0
- package/lib/config-resolvers/sources.js.map +1 -1
- package/lib/config.js +57 -0
- package/lib/config.js.map +1 -1
- package/lib/index.js +25 -10
- package/lib/index.js.map +1 -1
- package/lib/lockdown-policy.js +19 -0
- package/lib/lockdown-policy.js.map +1 -1
- package/lib/provenance/host-store.js +7 -3
- package/lib/provenance/host-store.js.map +1 -1
- package/lib/provenance/retention.js +25 -0
- package/lib/provenance/retention.js.map +1 -0
- package/lib/provenance/snapshot-store.js +290 -35
- package/lib/provenance/snapshot-store.js.map +1 -1
- package/lib/questions.js +16 -3
- package/lib/questions.js.map +1 -1
- package/lib/secure-session.js +136 -23
- package/lib/secure-session.js.map +1 -1
- package/lib/typert.host.js +79 -52
- package/lib/typert.remote-client.js +75 -48
- package/lib/types/access/role-repository.d.ts +38 -7
- package/lib/types/access/service.d.ts +77 -3
- package/lib/types/account-remotes.d.ts +8 -4
- package/lib/types/accounts/database.d.ts +84 -0
- package/lib/types/accounts/file.d.ts +17 -39
- package/lib/types/accounts/store.d.ts +98 -19
- package/lib/types/admin/quality-store.d.ts +20 -2
- package/lib/types/client/admin/{format.d.ts → copy.d.ts} +1 -1
- package/lib/types/client/components/Markdown.d.ts +7 -8
- package/lib/types/client/components/QaSidebar.d.ts +3 -1
- package/lib/types/client/components/QaSourcesPanel.d.ts +1 -3
- package/lib/types/client/components/source-icon.d.ts +12 -0
- package/lib/types/client/components/use-now.d.ts +8 -0
- package/lib/types/client/lineage.d.ts +15 -0
- package/lib/types/client/markdown/CodeBlock.d.ts +14 -0
- package/lib/types/client/markdown/blocks.d.ts +74 -0
- package/lib/types/client/markdown/highlight.d.ts +29 -0
- package/lib/types/client/markdown/inline.d.ts +40 -0
- package/lib/types/client/markdown/render.d.ts +13 -0
- package/lib/types/client/markdown/source-chip.d.ts +32 -0
- package/lib/types/client/markdown/use-transient-flag.d.ts +12 -0
- package/lib/types/client/role/RoleSelector.d.ts +11 -1
- package/lib/types/client/role/preview.d.ts +109 -0
- package/lib/types/index.d.ts +5 -1
- package/lib/types/lockdown-policy.d.ts +16 -0
- package/lib/types/provenance/host-store.d.ts +7 -3
- package/lib/types/provenance/retention.d.ts +46 -0
- package/lib/types/provenance/snapshot-store.d.ts +63 -7
- package/lib/types/questions.d.ts +5 -0
- package/lib/types/secure-session.d.ts +27 -1
- package/lib/types/types.d.ts +54 -0
- package/package.json +3 -3
- package/lib/client/admin/format.js.map +0 -1
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { readFileSync, renameSync } from "node:fs";
|
|
3
|
+
import { SqliteDatabase, } from "@yadsh/dsh-plugin-kit/sqlite";
|
|
4
|
+
import { snapshotDigest, } from "./file.js";
|
|
5
|
+
/** The token secret lives beside the schema version in the meta table. */
|
|
6
|
+
const SECRET_KEY = "token_secret";
|
|
7
|
+
/**
|
|
8
|
+
* One step per released layout. Versions are dense: a database records the
|
|
9
|
+
* highest one it has applied, and a build refuses to open a file from above it.
|
|
10
|
+
*/
|
|
11
|
+
const MIGRATIONS = [
|
|
12
|
+
{
|
|
13
|
+
version: 1,
|
|
14
|
+
up: `
|
|
15
|
+
CREATE TABLE qa_accounts (
|
|
16
|
+
id TEXT PRIMARY KEY,
|
|
17
|
+
seq INTEGER NOT NULL,
|
|
18
|
+
email TEXT NOT NULL,
|
|
19
|
+
display_name TEXT NOT NULL,
|
|
20
|
+
role TEXT NOT NULL,
|
|
21
|
+
disabled INTEGER NOT NULL DEFAULT 0,
|
|
22
|
+
token_version INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
created_at TEXT NOT NULL,
|
|
24
|
+
last_login_at TEXT,
|
|
25
|
+
password_salt TEXT NOT NULL,
|
|
26
|
+
password_hash TEXT NOT NULL,
|
|
27
|
+
profile_json TEXT,
|
|
28
|
+
starters_json TEXT,
|
|
29
|
+
qa_access_json TEXT
|
|
30
|
+
);
|
|
31
|
+
CREATE UNIQUE INDEX qa_accounts_email ON qa_accounts (email);
|
|
32
|
+
|
|
33
|
+
-- Every admitted chat freezes the capabilities it was admitted with, and
|
|
34
|
+
-- on a real deployment those policies are almost always the same list: 25
|
|
35
|
+
-- snapshots on the live stand were two distinct ones, and restating them
|
|
36
|
+
-- per chat was 45% of the file. One row per distinct policy, referenced.
|
|
37
|
+
CREATE TABLE qa_snapshots (
|
|
38
|
+
hash TEXT PRIMARY KEY,
|
|
39
|
+
json TEXT NOT NULL,
|
|
40
|
+
first_seen_at TEXT NOT NULL
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE qa_ownership (
|
|
44
|
+
session_id TEXT PRIMARY KEY,
|
|
45
|
+
user_id TEXT NOT NULL,
|
|
46
|
+
claimed_at TEXT NOT NULL,
|
|
47
|
+
subrole_id TEXT,
|
|
48
|
+
admin_preview INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
snapshot_hash TEXT REFERENCES qa_snapshots (hash)
|
|
50
|
+
);
|
|
51
|
+
CREATE INDEX qa_ownership_user ON qa_ownership (user_id, claimed_at DESC);
|
|
52
|
+
|
|
53
|
+
CREATE TABLE qa_skill_activations (
|
|
54
|
+
session_id TEXT NOT NULL,
|
|
55
|
+
seq INTEGER NOT NULL,
|
|
56
|
+
json TEXT NOT NULL,
|
|
57
|
+
PRIMARY KEY (session_id, seq)
|
|
58
|
+
);
|
|
59
|
+
`,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
/** SQLite hands back null-prototype records; the row shapes describe them. */
|
|
63
|
+
function asRows(value) {
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function parseJsonColumn(raw) {
|
|
67
|
+
if (raw === null || raw === "")
|
|
68
|
+
return undefined;
|
|
69
|
+
return JSON.parse(raw);
|
|
70
|
+
}
|
|
71
|
+
function toUser(row) {
|
|
72
|
+
const profile = parseJsonColumn(row.profile_json);
|
|
73
|
+
const starters = parseJsonColumn(row.starters_json);
|
|
74
|
+
const qaAccess = parseJsonColumn(row.qa_access_json);
|
|
75
|
+
return {
|
|
76
|
+
id: row.id,
|
|
77
|
+
email: row.email,
|
|
78
|
+
displayName: row.display_name,
|
|
79
|
+
role: row.role,
|
|
80
|
+
passwordHash: { salt: row.password_salt, hash: row.password_hash },
|
|
81
|
+
createdAt: row.created_at,
|
|
82
|
+
lastLoginAt: row.last_login_at,
|
|
83
|
+
...(row.disabled === 1 ? { disabled: true } : {}),
|
|
84
|
+
...(row.token_version === 0 ? {} : { tokenVersion: row.token_version }),
|
|
85
|
+
...(profile === undefined ? {} : { profile }),
|
|
86
|
+
...(starters === undefined ? {} : { starters }),
|
|
87
|
+
...(qaAccess === undefined ? {} : { qaAccess }),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The deployment's accounts, as tables.
|
|
92
|
+
*
|
|
93
|
+
* The stores this replaces were one JSON document read, parsed and rewritten
|
|
94
|
+
* whole on every mutation — so a chat being opened rewrote every account and
|
|
95
|
+
* every chat the deployment had ever seen, and the cost of that write grew with
|
|
96
|
+
* the deployment's age. Here a mutation touches the row it changes: opening a
|
|
97
|
+
* chat inserts one ownership row, and the file's size stops being a per-turn
|
|
98
|
+
* cost.
|
|
99
|
+
*
|
|
100
|
+
* Cross-process freshness survives the change. The `qa-accounts` CLI writes the
|
|
101
|
+
* same database from a second process, so the in-memory model is invalidated by
|
|
102
|
+
* `PRAGMA data_version`, which SQLite bumps exactly when *another* connection
|
|
103
|
+
* commits — the same guarantee the previous mtime-and-size stamp provided, with
|
|
104
|
+
* no file read to notice it.
|
|
105
|
+
*/
|
|
106
|
+
export class QaAccountsDatabase {
|
|
107
|
+
filePath;
|
|
108
|
+
storage;
|
|
109
|
+
observedDataVersion;
|
|
110
|
+
constructor(filePath) {
|
|
111
|
+
this.filePath = filePath;
|
|
112
|
+
this.storage = new SqliteDatabase(filePath, MIGRATIONS);
|
|
113
|
+
this.observedDataVersion = this.readDataVersion();
|
|
114
|
+
this.ensureSecret();
|
|
115
|
+
}
|
|
116
|
+
/** The token secret, creating one for a database that has none yet. */
|
|
117
|
+
get secret() {
|
|
118
|
+
const existing = this.storage.meta(SECRET_KEY);
|
|
119
|
+
if (existing !== undefined && existing !== "")
|
|
120
|
+
return existing;
|
|
121
|
+
throw new Error(`qa-accounts: ${this.filePath} has no token secret; refusing to mint tokens`);
|
|
122
|
+
}
|
|
123
|
+
/** True while the in-memory model a caller holds is still current. */
|
|
124
|
+
get unchangedSinceLoad() {
|
|
125
|
+
return this.readDataVersion() === this.observedDataVersion;
|
|
126
|
+
}
|
|
127
|
+
/** Take the current data version as the baseline a caller's model reflects. */
|
|
128
|
+
markLoaded() {
|
|
129
|
+
this.observedDataVersion = this.readDataVersion();
|
|
130
|
+
}
|
|
131
|
+
/** Every account, ownership record and skill activation, in one read. */
|
|
132
|
+
loadAll() {
|
|
133
|
+
const users = asRows(this.storage.db.prepare("SELECT * FROM qa_accounts ORDER BY seq").all());
|
|
134
|
+
const ownershipRows = asRows(this.storage.db
|
|
135
|
+
.prepare(`SELECT o.session_id, o.user_id, o.claimed_at, o.subrole_id,
|
|
136
|
+
o.admin_preview, s.json AS snapshot_json
|
|
137
|
+
FROM qa_ownership o
|
|
138
|
+
LEFT JOIN qa_snapshots s ON s.hash = o.snapshot_hash`)
|
|
139
|
+
.all());
|
|
140
|
+
const ownership = {};
|
|
141
|
+
for (const row of ownershipRows) {
|
|
142
|
+
const snapshot = parseJsonColumn(row.snapshot_json);
|
|
143
|
+
const skillActivations = asRows(this.storage.db
|
|
144
|
+
.prepare("SELECT json FROM qa_skill_activations WHERE session_id = ? ORDER BY seq")
|
|
145
|
+
.all(row.session_id));
|
|
146
|
+
ownership[row.session_id] = {
|
|
147
|
+
userId: row.user_id,
|
|
148
|
+
claimedAt: row.claimed_at,
|
|
149
|
+
...(row.subrole_id === null ? {} : { subroleId: row.subrole_id }),
|
|
150
|
+
...(row.admin_preview === 1 ? { adminPreview: true } : {}),
|
|
151
|
+
...(snapshot === undefined ? {} : { capabilitySnapshot: snapshot }),
|
|
152
|
+
...(skillActivations.length === 0
|
|
153
|
+
? {}
|
|
154
|
+
: {
|
|
155
|
+
skillActivations: skillActivations.map((entry) => JSON.parse(entry.json)),
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
this.markLoaded();
|
|
160
|
+
return {
|
|
161
|
+
version: 1,
|
|
162
|
+
secret: this.secret,
|
|
163
|
+
users: users.map(toUser),
|
|
164
|
+
ownership,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** Append one account, preserving the order accounts are listed in. */
|
|
168
|
+
insertUser(user) {
|
|
169
|
+
this.storage.transaction(() => {
|
|
170
|
+
this.writeUser(user);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Replace one account's row in place. */
|
|
174
|
+
updateUser(user) {
|
|
175
|
+
this.storage.transaction(() => {
|
|
176
|
+
this.writeUser(user, { replace: true });
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** Record a successful sign-in without rewriting the account. */
|
|
180
|
+
touchLogin(userId, at) {
|
|
181
|
+
this.storage.transaction(() => {
|
|
182
|
+
this.storage.db
|
|
183
|
+
.prepare("UPDATE qa_accounts SET last_login_at = ? WHERE id = ?")
|
|
184
|
+
.run(at, userId);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/** Claim one chat for an account. */
|
|
188
|
+
insertOwnership(sessionId, owner) {
|
|
189
|
+
this.storage.transaction(() => {
|
|
190
|
+
this.writeOwnership(sessionId, owner);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/** Claim many chats at once, which is how a browser's index migrates. */
|
|
194
|
+
insertOwnershipMany(rows) {
|
|
195
|
+
if (rows.length === 0)
|
|
196
|
+
return;
|
|
197
|
+
this.storage.transaction(() => {
|
|
198
|
+
for (const row of rows)
|
|
199
|
+
this.writeOwnership(row.sessionId, row.owner);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/** Rewrite an existing chat's pinned role, preview flag or snapshot. */
|
|
203
|
+
updateOwnership(sessionId, owner) {
|
|
204
|
+
this.storage.transaction(() => {
|
|
205
|
+
this.writeOwnership(sessionId, owner);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/** Forget chats: a rolled-back reservation, or a chat the Harness lost. */
|
|
209
|
+
deleteOwnership(sessionIds) {
|
|
210
|
+
if (sessionIds.length === 0)
|
|
211
|
+
return;
|
|
212
|
+
this.storage.transaction(() => {
|
|
213
|
+
const remove = this.storage.db.prepare("DELETE FROM qa_ownership WHERE session_id = ?");
|
|
214
|
+
for (const sessionId of sessionIds) {
|
|
215
|
+
remove.run(sessionId);
|
|
216
|
+
this.storage.db
|
|
217
|
+
.prepare("DELETE FROM qa_skill_activations WHERE session_id = ?")
|
|
218
|
+
.run(sessionId);
|
|
219
|
+
}
|
|
220
|
+
this.gcSnapshots();
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Append one skill activation and keep the newest `limit` of them. The list
|
|
225
|
+
* is bounded because a long session may activate skills many times, and the
|
|
226
|
+
* oldest entries stop being interesting long before the store does.
|
|
227
|
+
*/
|
|
228
|
+
appendSkillActivation(sessionId, entry, limit) {
|
|
229
|
+
return this.storage.transaction(() => {
|
|
230
|
+
const next = this.storage.db
|
|
231
|
+
.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM qa_skill_activations WHERE session_id = ?")
|
|
232
|
+
.get(sessionId);
|
|
233
|
+
this.storage.db
|
|
234
|
+
.prepare("INSERT INTO qa_skill_activations (session_id, seq, json) VALUES (?, ?, ?)")
|
|
235
|
+
.run(sessionId, next.next, JSON.stringify(entry));
|
|
236
|
+
if (limit > 0) {
|
|
237
|
+
this.storage.db
|
|
238
|
+
.prepare(`DELETE FROM qa_skill_activations
|
|
239
|
+
WHERE session_id = ?
|
|
240
|
+
AND seq <= ? - ?`)
|
|
241
|
+
.run(sessionId, next.next, limit);
|
|
242
|
+
}
|
|
243
|
+
const kept = asRows(this.storage.db
|
|
244
|
+
.prepare("SELECT json FROM qa_skill_activations WHERE session_id = ? ORDER BY seq")
|
|
245
|
+
.all(sessionId));
|
|
246
|
+
return kept.map((row) => JSON.parse(row.json));
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Import a pre-SQLite `qa-accounts.json` exactly once, then rename it away.
|
|
251
|
+
*
|
|
252
|
+
* The import is verified *inside* the transaction and before the file is
|
|
253
|
+
* retired — every account and every ownership record must have arrived, and
|
|
254
|
+
* the token secret must be the one the file carried — because a silent
|
|
255
|
+
* partial import would either lock accounts out or hand a chat to the wrong
|
|
256
|
+
* owner. A database that already holds accounts is never overwritten by a
|
|
257
|
+
* leftover file.
|
|
258
|
+
*/
|
|
259
|
+
importLegacyFile(legacyFilePath) {
|
|
260
|
+
if (legacyFilePath === undefined)
|
|
261
|
+
return undefined;
|
|
262
|
+
let raw;
|
|
263
|
+
try {
|
|
264
|
+
raw = readFileSync(legacyFilePath, "utf8");
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (error.code === "ENOENT")
|
|
268
|
+
return undefined;
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
const legacy = this.parseLegacyFile(legacyFilePath, raw);
|
|
272
|
+
if (this.hasAccounts())
|
|
273
|
+
return undefined;
|
|
274
|
+
const imported = this.storage.transaction(() => {
|
|
275
|
+
this.storage.setMeta(SECRET_KEY, legacy.secret);
|
|
276
|
+
for (const user of legacy.users)
|
|
277
|
+
this.writeUser(user);
|
|
278
|
+
for (const [sessionId, owner] of Object.entries(legacy.ownership)) {
|
|
279
|
+
this.writeOwnership(sessionId, owner);
|
|
280
|
+
}
|
|
281
|
+
const arrived = this.loadAll();
|
|
282
|
+
this.assertImportArrived(legacyFilePath, legacy, arrived);
|
|
283
|
+
return arrived;
|
|
284
|
+
});
|
|
285
|
+
renameSync(legacyFilePath, `${legacyFilePath}.migrated-${new Date().toISOString().replace(/[:.]/gu, "-")}`);
|
|
286
|
+
return imported;
|
|
287
|
+
}
|
|
288
|
+
assertImportArrived(legacyFilePath, legacy, imported) {
|
|
289
|
+
const problems = [];
|
|
290
|
+
if (imported.secret !== legacy.secret) {
|
|
291
|
+
problems.push("the token secret did not arrive");
|
|
292
|
+
}
|
|
293
|
+
if (imported.users.length !== legacy.users.length) {
|
|
294
|
+
problems.push(`expected ${legacy.users.length} accounts, imported ${imported.users.length}`);
|
|
295
|
+
}
|
|
296
|
+
const expectedSessions = Object.keys(legacy.ownership).length;
|
|
297
|
+
const importedSessions = Object.keys(imported.ownership).length;
|
|
298
|
+
if (importedSessions !== expectedSessions) {
|
|
299
|
+
problems.push(`expected ${expectedSessions} chat records, imported ${importedSessions}`);
|
|
300
|
+
}
|
|
301
|
+
if (problems.length > 0) {
|
|
302
|
+
throw new Error(`qa-accounts: importing ${legacyFilePath} failed verification (${problems.join("; ")}); the file is left in place and the import was rolled back`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
close() {
|
|
306
|
+
this.storage.close();
|
|
307
|
+
}
|
|
308
|
+
hasAccounts() {
|
|
309
|
+
const row = this.storage.db
|
|
310
|
+
.prepare("SELECT COUNT(*) AS count FROM qa_accounts")
|
|
311
|
+
.get();
|
|
312
|
+
return row.count > 0;
|
|
313
|
+
}
|
|
314
|
+
readDataVersion() {
|
|
315
|
+
const row = this.storage.db.prepare("PRAGMA data_version").get();
|
|
316
|
+
return row.data_version;
|
|
317
|
+
}
|
|
318
|
+
ensureSecret() {
|
|
319
|
+
const existing = this.storage.meta(SECRET_KEY);
|
|
320
|
+
if (existing !== undefined && existing !== "")
|
|
321
|
+
return;
|
|
322
|
+
this.storage.transaction(() => {
|
|
323
|
+
// 32 random bytes, base64url — the shape the tokens already assume.
|
|
324
|
+
this.storage.setMeta(SECRET_KEY, randomBytes(32).toString("base64url"));
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/** The account columns, in the order both write statements bind them. */
|
|
328
|
+
accountColumns(user) {
|
|
329
|
+
return [
|
|
330
|
+
user.email,
|
|
331
|
+
user.displayName,
|
|
332
|
+
user.role,
|
|
333
|
+
user.disabled === true ? 1 : 0,
|
|
334
|
+
user.tokenVersion ?? 0,
|
|
335
|
+
user.createdAt,
|
|
336
|
+
user.lastLoginAt,
|
|
337
|
+
user.passwordHash.salt,
|
|
338
|
+
user.passwordHash.hash,
|
|
339
|
+
user.profile === undefined ? null : JSON.stringify(user.profile),
|
|
340
|
+
user.starters === undefined ? null : JSON.stringify(user.starters),
|
|
341
|
+
user.qaAccess === undefined ? null : JSON.stringify(user.qaAccess),
|
|
342
|
+
];
|
|
343
|
+
}
|
|
344
|
+
writeUser(user, options = {}) {
|
|
345
|
+
const columns = this.accountColumns(user);
|
|
346
|
+
if (options.replace === true) {
|
|
347
|
+
this.storage.db
|
|
348
|
+
.prepare(`UPDATE qa_accounts
|
|
349
|
+
SET email = ?, display_name = ?, role = ?, disabled = ?,
|
|
350
|
+
token_version = ?, created_at = ?, last_login_at = ?,
|
|
351
|
+
password_salt = ?, password_hash = ?, profile_json = ?,
|
|
352
|
+
starters_json = ?, qa_access_json = ?
|
|
353
|
+
WHERE id = ?`)
|
|
354
|
+
.run(...columns, user.id);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// A new account lands after every account already stored, which is the
|
|
358
|
+
// order the operator CLI and the console list them in.
|
|
359
|
+
const next = this.storage.db
|
|
360
|
+
.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM qa_accounts")
|
|
361
|
+
.get();
|
|
362
|
+
this.storage.db
|
|
363
|
+
.prepare(`INSERT INTO qa_accounts
|
|
364
|
+
(id, seq, email, display_name, role, disabled, token_version,
|
|
365
|
+
created_at, last_login_at, password_salt, password_hash,
|
|
366
|
+
profile_json, starters_json, qa_access_json)
|
|
367
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
368
|
+
.run(user.id, next.next, ...columns);
|
|
369
|
+
}
|
|
370
|
+
writeOwnership(sessionId, owner) {
|
|
371
|
+
const snapshotHash = owner.capabilitySnapshot === undefined
|
|
372
|
+
? null
|
|
373
|
+
: this.writeSnapshot(owner.capabilitySnapshot);
|
|
374
|
+
this.storage.db
|
|
375
|
+
.prepare(`INSERT INTO qa_ownership
|
|
376
|
+
(session_id, user_id, claimed_at, subrole_id, admin_preview, snapshot_hash)
|
|
377
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
378
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
379
|
+
user_id = excluded.user_id,
|
|
380
|
+
claimed_at = excluded.claimed_at,
|
|
381
|
+
subrole_id = excluded.subrole_id,
|
|
382
|
+
admin_preview = excluded.admin_preview,
|
|
383
|
+
snapshot_hash = excluded.snapshot_hash`)
|
|
384
|
+
.run(sessionId, owner.userId, owner.claimedAt, owner.subroleId ?? null, owner.adminPreview === true ? 1 : 0, snapshotHash);
|
|
385
|
+
const activations = owner.skillActivations ?? [];
|
|
386
|
+
this.storage.db
|
|
387
|
+
.prepare("DELETE FROM qa_skill_activations WHERE session_id = ?")
|
|
388
|
+
.run(sessionId);
|
|
389
|
+
const insert = this.storage.db.prepare("INSERT INTO qa_skill_activations (session_id, seq, json) VALUES (?, ?, ?)");
|
|
390
|
+
activations.forEach((entry, index) => {
|
|
391
|
+
insert.run(sessionId, index + 1, JSON.stringify(entry));
|
|
392
|
+
});
|
|
393
|
+
this.gcSnapshots();
|
|
394
|
+
}
|
|
395
|
+
/** One row per distinct policy; the digest is the policy's content. */
|
|
396
|
+
writeSnapshot(policy) {
|
|
397
|
+
const hash = snapshotDigest(policy);
|
|
398
|
+
this.storage.db
|
|
399
|
+
.prepare(`INSERT INTO qa_snapshots (hash, json, first_seen_at) VALUES (?, ?, ?)
|
|
400
|
+
ON CONFLICT(hash) DO NOTHING`)
|
|
401
|
+
.run(hash, JSON.stringify(policy), new Date().toISOString());
|
|
402
|
+
return hash;
|
|
403
|
+
}
|
|
404
|
+
/** Drop snapshots no chat refers to any more, so the table cannot drift up. */
|
|
405
|
+
gcSnapshots() {
|
|
406
|
+
this.storage.db
|
|
407
|
+
.prepare(`DELETE FROM qa_snapshots
|
|
408
|
+
WHERE hash NOT IN (
|
|
409
|
+
SELECT snapshot_hash FROM qa_ownership
|
|
410
|
+
WHERE snapshot_hash IS NOT NULL
|
|
411
|
+
)`)
|
|
412
|
+
.run();
|
|
413
|
+
}
|
|
414
|
+
parseLegacyFile(filePath, raw) {
|
|
415
|
+
const parsed = JSON.parse(raw);
|
|
416
|
+
if (typeof parsed !== "object" ||
|
|
417
|
+
parsed === null ||
|
|
418
|
+
parsed.version !== 1 ||
|
|
419
|
+
typeof parsed.secret !== "string" ||
|
|
420
|
+
!Array.isArray(parsed.users)) {
|
|
421
|
+
throw new Error(`qa-accounts: ${filePath} is not a recognizable accounts file; refusing to import it`);
|
|
422
|
+
}
|
|
423
|
+
const file = parsed;
|
|
424
|
+
const snapshots = file.snapshots ?? {};
|
|
425
|
+
const ownership = {};
|
|
426
|
+
for (const [sessionId, entry] of Object.entries(file.ownership ?? {})) {
|
|
427
|
+
const stored = entry;
|
|
428
|
+
const { snapshotRef, ...rest } = stored;
|
|
429
|
+
if (snapshotRef === undefined) {
|
|
430
|
+
ownership[sessionId] = rest;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const snapshot = snapshots[snapshotRef];
|
|
434
|
+
if (snapshot === undefined) {
|
|
435
|
+
throw new Error(`qa-accounts: session ${sessionId} refers to missing capability snapshot ${snapshotRef}; refusing to import a corrupt accounts file`);
|
|
436
|
+
}
|
|
437
|
+
ownership[sessionId] = { ...rest, capabilitySnapshot: snapshot };
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
version: 1,
|
|
441
|
+
secret: file.secret,
|
|
442
|
+
users: file.users,
|
|
443
|
+
ownership,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/accounts/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EACL,cAAc,GAEf,MAAM,8BAA8B,CAAC;AAMtC,OAAO,EACL,cAAc,GAMf,MAAM,WAAW,CAAC;AAEnB,0EAA0E;AAC1E,MAAM,UAAU,GAAG,cAAc,CAAC;AAElC;;;GAGG;AACH,MAAM,UAAU,GAA+B;IAC7C;QACE,OAAO,EAAE,CAAC;QACV,EAAE,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA6CH;KACF;CACF,CAAC;AAiCF,8EAA8E;AAC9E,SAAS,MAAM,CAAI,KAAc;IAC/B,OAAO,KAAY,CAAC;AACtB,CAAC;AAED,SAAS,eAAe,CAAI,GAAkB;IAC5C,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACjD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAM,CAAC;AAC9B,CAAC;AAED,SAAS,MAAM,CAAC,GAAe;IAC7B,MAAM,OAAO,GAAG,eAAe,CAAgB,GAAG,CAAC,YAAY,CAAC,CAAC;IACjE,MAAM,QAAQ,GAAG,eAAe,CAAiB,GAAG,CAAC,aAAa,CAAC,CAAC;IACpE,MAAM,QAAQ,GAAG,eAAe,CAAe,GAAG,CAAC,cAAc,CAAC,CAAC;IACnE,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,WAAW,EAAE,GAAG,CAAC,YAAY;QAC7B,IAAI,EAAE,GAAG,CAAC,IAA0B;QACpC,YAAY,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,aAAa,EAAE,IAAI,EAAE,GAAG,CAAC,aAAa,EAAE;QAClE,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,WAAW,EAAE,GAAG,CAAC,aAAa;QAC9B,GAAG,CAAC,GAAG,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjD,GAAG,CAAC,GAAG,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,CAAC,aAAa,EAAE,CAAC;QACvE,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;QAC7C,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;QAC/C,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;KAChD,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,kBAAkB;IAIR,QAAQ;IAHpB,OAAO,CAAiB;IACzB,mBAAmB,CAAS;IAEpC,YAAqB,QAAgB;wBAAhB,QAAQ;QAC3B,IAAI,CAAC,OAAO,GAAG,IAAI,cAAc,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QACxD,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QAClD,IAAI,CAAC,YAAY,EAAE,CAAC;IACtB,CAAC;IAED,uEAAuE;IACvE,IAAI,MAAM;QACR,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC/C,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,EAAE;YAAE,OAAO,QAAQ,CAAC;QAC/D,MAAM,IAAI,KAAK,CACb,gBAAgB,IAAI,CAAC,QAAQ,+CAA+C,CAC7E,CAAC;IACJ,CAAC;IAED,sEAAsE;IACtE,IAAI,kBAAkB;QACpB,OAAO,IAAI,CAAC,eAAe,EAAE,KAAK,IAAI,CAAC,mBAAmB,CAAC;IAC7D,CAAC;IAED,+EAA+E;IAC/E,UAAU;QACR,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;IACpD,CAAC;IAED,yEAAyE;IACzE,OAAO;QACL,MAAM,KAAK,GAAG,MAAM,CAClB,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,wCAAwC,CAAC,CAAC,GAAG,EAAE,CACxE,CAAC;QACF,MAAM,aAAa,GAAG,MAAM,CAC1B,IAAI,CAAC,OAAO,CAAC,EAAE;aACZ,OAAO,CACN;;;kEAGwD,CACzD;aACA,GAAG,EAAE,CACT,CAAC;QACF,MAAM,SAAS,GAAoC,EAAE,CAAC;QACtD,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;YAChC,MAAM,QAAQ,GAAG,eAAe,CAC9B,GAAG,CAAC,aAAa,CAClB,CAAC;YACF,MAAM,gBAAgB,GAAG,MAAM,CAC7B,IAAI,CAAC,OAAO,CAAC,EAAE;iBACZ,OAAO,CACN,yEAAyE,CAC1E;iBACA,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CACvB,CAAC;YACF,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG;gBAC1B,MAAM,EAAE,GAAG,CAAC,OAAO;gBACnB,SAAS,EAAE,GAAG,CAAC,UAAU;gBACzB,GAAG,CAAC,GAAG,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC;gBACjE,GAAG,CAAC,GAAG,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1D,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,QAAQ,EAAE,CAAC;gBACnE,GAAG,CAAC,gBAAgB,CAAC,MAAM,KAAK,CAAC;oBAC/B,CAAC,CAAC,EAAE;oBACJ,CAAC,CAAC;wBACE,gBAAgB,EAAE,gBAAgB,CAAC,GAAG,CACpC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAA4B,CAC7D;qBACF,CAAC;aACP,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,OAAO;YACL,OAAO,EAAE,CAAC;YACV,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;YACxB,SAAS;SACV,CAAC;IACJ,CAAC;IAED,uEAAuE;IACvE,UAAU,CAAC,IAAgB;QACzB,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,0CAA0C;IAC1C,UAAU,CAAC,IAAgB;QACzB,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;IACL,CAAC;IAED,iEAAiE;IACjE,UAAU,CAAC,MAAc,EAAE,EAAU;QACnC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,OAAO,CAAC,EAAE;iBACZ,OAAO,CAAC,uDAAuD,CAAC;iBAChE,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,qCAAqC;IACrC,eAAe,CAAC,SAAiB,EAAE,KAAsB;QACvD,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,yEAAyE;IACzE,mBAAmB,CAAC,IAA+B;QACjD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC9B,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE;YAC5B,KAAK,MAAM,GAAG,IAAI,IAAI;gBAAE,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QACxE,CAAC,CAAC,CAAC;IACL,CAAC;IAED,wEAAwE;IACxE,eAAe,CAAC,SAAiB,EAAE,KAAsB;QACvD,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,2EAA2E;IAC3E,eAAe,CAAC,UAA6B;QAC3C,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACpC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE;YAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CACpC,+CAA+C,CAChD,CAAC;YACF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;gBACnC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBACtB,IAAI,CAAC,OAAO,CAAC,EAAE;qBACZ,OAAO,CAAC,uDAAuD,CAAC;qBAChE,GAAG,CAAC,SAAS,CAAC,CAAC;YACpB,CAAC;YACD,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,qBAAqB,CACnB,SAAiB,EACjB,KAA8B,EAC9B,KAAa;QAEb,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE;YACnC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE;iBACzB,OAAO,CACN,yFAAyF,CAC1F;iBACA,GAAG,CAAC,SAAS,CAAqB,CAAC;YACtC,IAAI,CAAC,OAAO,CAAC,EAAE;iBACZ,OAAO,CACN,2EAA2E,CAC5E;iBACA,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YACpD,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;gBACd,IAAI,CAAC,OAAO,CAAC,EAAE;qBACZ,OAAO,CACN;;iCAEqB,CACtB;qBACA,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACtC,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,CACjB,IAAI,CAAC,OAAO,CAAC,EAAE;iBACZ,OAAO,CACN,yEAAyE,CAC1E;iBACA,GAAG,CAAC,SAAS,CAAC,CAClB,CAAC;YACF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAA4B,CAAC,CAAC;QAC5E,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;OASG;IACH,gBAAgB,CACd,cAAkC;QAElC,IAAI,cAAc,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QACnD,IAAI,GAAW,CAAC;QAChB,IAAI,CAAC;YACH,GAAG,GAAG,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QAC7C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,SAAS,CAAC;YACzE,MAAM,KAAK,CAAC;QACd,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;QACzD,IAAI,IAAI,CAAC,WAAW,EAAE;YAAE,OAAO,SAAS,CAAC;QACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE;YAC7C,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;YAChD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK;gBAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YACtD,KAAK,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;gBAClE,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;YACxC,CAAC;YACD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;YAC/B,IAAI,CAAC,mBAAmB,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;YAC1D,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC,CAAC;QACH,UAAU,CACR,cAAc,EACd,GAAG,cAAc,aAAa,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,CAChF,CAAC;QACF,OAAO,QAAQ,CAAC;IAClB,CAAC;IAEO,mBAAmB,CACzB,cAAsB,EACtB,MAAoB,EACpB,QAAsB;QAEtB,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;YACtC,QAAQ,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;QACnD,CAAC;QACD,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YAClD,QAAQ,CAAC,IAAI,CACX,YAAY,MAAM,CAAC,KAAK,CAAC,MAAM,uBAAuB,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,CAC9E,CAAC;QACJ,CAAC;QACD,MAAM,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC;QAC9D,MAAM,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC;QAChE,IAAI,gBAAgB,KAAK,gBAAgB,EAAE,CAAC;YAC1C,QAAQ,CAAC,IAAI,CACX,YAAY,gBAAgB,2BAA2B,gBAAgB,EAAE,CAC1E,CAAC;QACJ,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CACb,0BAA0B,cAAc,yBAAyB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,6DAA6D,CAClJ,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAEO,WAAW;QACjB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE;aACxB,OAAO,CAAC,2CAA2C,CAAC;aACpD,GAAG,EAAuB,CAAC;QAC9B,OAAO,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC;IACvB,CAAC;IAEO,eAAe;QACrB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,GAAG,EAE7D,CAAC;QACF,OAAO,GAAG,CAAC,YAAY,CAAC;IAC1B,CAAC;IAEO,YAAY;QAClB,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC/C,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,EAAE;YAAE,OAAO;QACtD,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE;YAC5B,oEAAoE;YACpE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;IACL,CAAC;IAED,yEAAyE;IACjE,cAAc,CACpB,IAAgB;QAEhB,OAAO;YACL,IAAI,CAAC,KAAK;YACV,IAAI,CAAC,WAAW;YAChB,IAAI,CAAC,IAAI;YACT,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9B,IAAI,CAAC,YAAY,IAAI,CAAC;YACtB,IAAI,CAAC,SAAS;YACd,IAAI,CAAC,WAAW;YAChB,IAAI,CAAC,YAAY,CAAC,IAAI;YACtB,IAAI,CAAC,YAAY,CAAC,IAAI;YACtB,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;YAChE,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC;YAClE,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC;SACnE,CAAC;IACJ,CAAC;IAEO,SAAS,CACf,IAAgB,EAChB,OAAO,GAA0B,EAAE;QAEnC,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YAC7B,IAAI,CAAC,OAAO,CAAC,EAAE;iBACZ,OAAO,CACN;;;;;yBAKe,CAChB;iBACA,GAAG,CAAC,GAAG,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;YAC5B,OAAO;QACT,CAAC;QACD,uEAAuE;QACvE,uDAAuD;QACvD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE;aACzB,OAAO,CAAC,2DAA2D,CAAC;aACpE,GAAG,EAAsB,CAAC;QAC7B,IAAI,CAAC,OAAO,CAAC,EAAE;aACZ,OAAO,CACN;;;;2DAImD,CACpD;aACA,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC,CAAC;IACzC,CAAC;IAEO,cAAc,CAAC,SAAiB,EAAE,KAAsB;QAC9D,MAAM,YAAY,GAChB,KAAK,CAAC,kBAAkB,KAAK,SAAS;YACpC,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;QACnD,IAAI,CAAC,OAAO,CAAC,EAAE;aACZ,OAAO,CACN;;;;;;;;kDAQ0C,CAC3C;aACA,GAAG,CACF,SAAS,EACT,KAAK,CAAC,MAAM,EACZ,KAAK,CAAC,SAAS,EACf,KAAK,CAAC,SAAS,IAAI,IAAI,EACvB,KAAK,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EACnC,YAAY,CACb,CAAC;QACJ,MAAM,WAAW,GAAG,KAAK,CAAC,gBAAgB,IAAI,EAAE,CAAC;QACjD,IAAI,CAAC,OAAO,CAAC,EAAE;aACZ,OAAO,CAAC,uDAAuD,CAAC;aAChE,GAAG,CAAC,SAAS,CAAC,CAAC;QAClB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CACpC,2EAA2E,CAC5E,CAAC;QACF,WAAW,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;YACnC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1D,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,WAAW,EAAE,CAAC;IACrB,CAAC;IAED,uEAAuE;IAC/D,aAAa,CAAC,MAAmC;QACvD,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,CAAC,OAAO,CAAC,EAAE;aACZ,OAAO,CACN;sCAC8B,CAC/B;aACA,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;QAC/D,OAAO,IAAI,CAAC;IACd,CAAC;IAED,+EAA+E;IACvE,WAAW;QACjB,IAAI,CAAC,OAAO,CAAC,EAAE;aACZ,OAAO,CACN;;;;YAII,CACL;aACA,GAAG,EAAE,CAAC;IACX,CAAC;IAEO,eAAe,CAAC,QAAgB,EAAE,GAAW;QACnD,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,IACE,OAAO,MAAM,KAAK,QAAQ;YAC1B,MAAM,KAAK,IAAI;YACd,MAAuB,CAAC,OAAO,KAAK,CAAC;YACtC,OAAQ,MAAuB,CAAC,MAAM,KAAK,QAAQ;YACnD,CAAC,KAAK,CAAC,OAAO,CAAE,MAAuB,CAAC,KAAK,CAAC,EAC9C,CAAC;YACD,MAAM,IAAI,KAAK,CACb,gBAAgB,QAAQ,6DAA6D,CACtF,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,MAEZ,CAAC;QACF,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC;QACvC,MAAM,SAAS,GAAoC,EAAE,CAAC;QACtD,KAAK,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,EAAE,CAAC;YACtE,MAAM,MAAM,GAAG,KAAmD,CAAC;YACnE,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC;YACxC,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;gBAC9B,SAAS,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;gBAC5B,SAAS;YACX,CAAC;YACD,MAAM,QAAQ,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC;YACxC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,MAAM,IAAI,KAAK,CACb,wBAAwB,SAAS,0CAA0C,WAAW,8CAA8C,CACrI,CAAC;YACJ,CAAC;YACD,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,IAAI,EAAE,kBAAkB,EAAE,QAAQ,EAAE,CAAC;QACnE,CAAC;QACD,OAAO;YACL,OAAO,EAAE,CAAC;YACV,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,SAAS;SACV,CAAC;IACJ,CAAC;CACF"}
|
package/lib/accounts/file.js
CHANGED
|
@@ -1,101 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
/** A stamp no real file can match, so the first load always reads the disk. */
|
|
6
|
-
export function createFileStampRef() {
|
|
7
|
-
return { current: { mtimeMs: Number.NEGATIVE_INFINITY, size: -1 } };
|
|
8
|
-
}
|
|
9
|
-
export function stampOf(stat) {
|
|
10
|
-
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
11
|
-
}
|
|
12
|
-
const SECRET_BYTES = 32;
|
|
13
|
-
/** The accounts file lives next to the DSH home the launcher exports. */
|
|
3
|
+
/** The accounts database lives next to the DSH home the launcher exports. */
|
|
14
4
|
export function defaultAccountsFilePath() {
|
|
15
5
|
const home = process.env.DSH_HOME?.trim();
|
|
16
|
-
return path.join(home !== undefined && home !== "" ? home : process.cwd(), "qa-accounts.
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Read the accounts file, or create it on first run with a fresh token
|
|
20
|
-
* secret. The stamp ref is left pointing at the version just read.
|
|
21
|
-
*/
|
|
22
|
-
export function loadAccountsFile(filePath, stamp) {
|
|
23
|
-
let raw;
|
|
24
|
-
let stat;
|
|
25
|
-
try {
|
|
26
|
-
raw = readFileSync(filePath, "utf8");
|
|
27
|
-
stat = statSync(filePath);
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
// Only a missing file means "first run": anything else (permissions,
|
|
31
|
-
// a directory in the way, I/O failure) must surface, or the store
|
|
32
|
-
// would silently reset every account it cannot read.
|
|
33
|
-
if (error.code !== "ENOENT")
|
|
34
|
-
throw error;
|
|
35
|
-
const created = {
|
|
36
|
-
version: 1,
|
|
37
|
-
secret: base64Url(randomBytes(SECRET_BYTES)),
|
|
38
|
-
users: [],
|
|
39
|
-
ownership: {},
|
|
40
|
-
};
|
|
41
|
-
persistAccountsFile(filePath, created, stamp);
|
|
42
|
-
return created;
|
|
43
|
-
}
|
|
44
|
-
const parsed = JSON.parse(raw);
|
|
45
|
-
if (typeof parsed !== "object" ||
|
|
46
|
-
parsed === null ||
|
|
47
|
-
parsed.version !== 1 ||
|
|
48
|
-
typeof parsed.secret !== "string" ||
|
|
49
|
-
!Array.isArray(parsed.users)) {
|
|
50
|
-
throw new Error(`qa-accounts: ${filePath} is not a recognizable accounts file; refusing to overwrite it`);
|
|
51
|
-
}
|
|
52
|
-
const file = parsed;
|
|
53
|
-
if (stat !== undefined)
|
|
54
|
-
stamp.current = stampOf(stat);
|
|
55
|
-
return {
|
|
56
|
-
version: 1,
|
|
57
|
-
secret: file.secret,
|
|
58
|
-
users: file.users,
|
|
59
|
-
ownership: file.ownership ?? {},
|
|
60
|
-
};
|
|
6
|
+
return path.join(home !== undefined && home !== "" ? home : process.cwd(), "qa-accounts.db");
|
|
61
7
|
}
|
|
62
8
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
9
|
+
* Where the pre-SQLite accounts file lived. It is imported on first use and
|
|
10
|
+
* then renamed aside, so a deployment upgrades without re-registering anyone.
|
|
65
11
|
*/
|
|
66
|
-
export function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const temp = `${filePath}.${process.pid}.tmp`;
|
|
70
|
-
writeFileSync(temp, `${JSON.stringify(file, null, 2)}\n`, "utf8");
|
|
71
|
-
renameSync(temp, filePath);
|
|
72
|
-
stamp.current = stampOf(statSync(filePath));
|
|
12
|
+
export function defaultLegacyAccountsFilePath() {
|
|
13
|
+
const home = process.env.DSH_HOME?.trim();
|
|
14
|
+
return path.join(home !== undefined && home !== "" ? home : process.cwd(), "qa-accounts.json");
|
|
73
15
|
}
|
|
74
16
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* and its next save would resurrect the CLI's change from a stale snapshot.
|
|
79
|
-
* One file, one writer at a time remains the deployment's discipline; LAN
|
|
80
|
-
* scale keeps the stat per operation trivial.
|
|
81
|
-
*
|
|
82
|
-
* Returns the freshly loaded file, or undefined when the caller should keep
|
|
83
|
-
* serving its in-memory state.
|
|
17
|
+
* A stable digest of one capability policy, so two chats admitted under the
|
|
18
|
+
* same policy share a stored snapshot. Keys are sorted because object key order
|
|
19
|
+
* is an artefact of construction, not part of the policy.
|
|
84
20
|
*/
|
|
85
|
-
export function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
21
|
+
export function snapshotDigest(value) {
|
|
22
|
+
const canonical = (input) => {
|
|
23
|
+
if (input === null || typeof input !== "object") {
|
|
24
|
+
return JSON.stringify(input) ?? "null";
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(input)) {
|
|
27
|
+
return `[${input.map(canonical).join(",")}]`;
|
|
28
|
+
}
|
|
29
|
+
const entries = Object.entries(input)
|
|
30
|
+
.filter(([, entry]) => entry !== undefined)
|
|
31
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
32
|
+
return `{${entries
|
|
33
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonical(entry)}`)
|
|
34
|
+
.join(",")}}`;
|
|
35
|
+
};
|
|
36
|
+
return createHash("sha256").update(canonical(value)).digest("hex");
|
|
100
37
|
}
|
|
101
38
|
//# sourceMappingURL=file.js.map
|
package/lib/accounts/file.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.js","sourceRoot":"","sources":["../../src/accounts/file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"file.js","sourceRoot":"","sources":["../../src/accounts/file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAyE7B,6EAA6E;AAC7E,MAAM,UAAU,uBAAuB;IACrC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1C,OAAO,IAAI,CAAC,IAAI,CACd,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,EACxD,gBAAgB,CACjB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,6BAA6B;IAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1C,OAAO,IAAI,CAAC,IAAI,CACd,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,EACxD,kBAAkB,CACnB,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,MAAM,SAAS,GAAG,CAAC,KAAc,EAAU,EAAE;QAC3C,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAChD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC;QACzC,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QAC/C,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC;aAC7D,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC;aAC1C,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzE,OAAO,IAAI,OAAO;aACf,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;aACnE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACrE,CAAC"}
|