@yadsh/dsh-qa-surface 0.1.1 → 0.3.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/CHANGELOG.md +151 -0
- package/README.md +239 -16
- package/SPEC-dsh-qa-surface.md +600 -96
- package/capability-policy.json +5 -0
- package/compatibility.json +18 -4
- package/cordis.patch.yml +11 -0
- package/docs/ARCHITECTURE.md +50 -8
- package/docs/COMPATIBILITY.md +5 -4
- package/docs/CONFIGURATION.md +184 -3
- package/docs/SOURCES-MIGRATION.md +28 -0
- package/docs/SOURCES-PROVENANCE-AUDIT.md +88 -0
- package/docs/SPEC-dsh-qa-surface-accounts.md +219 -0
- package/docs/SPEC-dsh-qa-surface-sources-provenance.md +1692 -0
- package/lib/accounts/index.js +2 -0
- package/lib/accounts/index.js.map +1 -0
- package/lib/accounts/store.js +478 -0
- package/lib/accounts/store.js.map +1 -0
- package/lib/attestation.js +10 -0
- package/lib/attestation.js.map +1 -0
- package/lib/cli.js +143 -0
- package/lib/cli.js.map +1 -0
- package/lib/client/QaAccountsController.js +329 -0
- package/lib/client/QaAccountsController.js.map +1 -0
- package/lib/client/QaConfigController.js +111 -4
- package/lib/client/QaConfigController.js.map +1 -1
- package/lib/client/QaSessionController.js +621 -163
- package/lib/client/QaSessionController.js.map +1 -1
- package/lib/client/QaSurface.js +303 -24
- package/lib/client/QaSurface.js.map +1 -1
- package/lib/client/QaTranscriptAdapter.js +324 -49
- package/lib/client/QaTranscriptAdapter.js.map +1 -1
- package/lib/client/attestation.js +66 -0
- package/lib/client/attestation.js.map +1 -0
- package/lib/client/chat-index.js +86 -0
- package/lib/client/chat-index.js.map +1 -0
- package/lib/client/components/Markdown.js +124 -13
- package/lib/client/components/Markdown.js.map +1 -1
- package/lib/client/components/QaAgentsDrawer.js +40 -0
- package/lib/client/components/QaAgentsDrawer.js.map +1 -0
- package/lib/client/components/QaAuthGate.js +40 -0
- package/lib/client/components/QaAuthGate.js.map +1 -0
- package/lib/client/components/QaChangelog.js +112 -0
- package/lib/client/components/QaChangelog.js.map +1 -0
- package/lib/client/components/QaComposer.js +132 -15
- package/lib/client/components/QaComposer.js.map +1 -1
- package/lib/client/components/QaMessage.js +214 -10
- package/lib/client/components/QaMessage.js.map +1 -1
- package/lib/client/components/QaSidebar.js +167 -0
- package/lib/client/components/QaSidebar.js.map +1 -0
- package/lib/client/components/QaSourcesDrawer.js +127 -0
- package/lib/client/components/QaSourcesDrawer.js.map +1 -0
- package/lib/client/components/QaTurnRail.js +281 -0
- package/lib/client/components/QaTurnRail.js.map +1 -0
- package/lib/client/components/QaWelcomeNotice.js +125 -0
- package/lib/client/components/QaWelcomeNotice.js.map +1 -0
- package/lib/client/components/QaWidthHandle.js +142 -0
- package/lib/client/components/QaWidthHandle.js.map +1 -0
- package/lib/client/components/QaWorkGroup.js +135 -0
- package/lib/client/components/QaWorkGroup.js.map +1 -0
- package/lib/client/components/VariantSwitcher.js +29 -0
- package/lib/client/components/VariantSwitcher.js.map +1 -0
- package/lib/client/components/format.js +48 -0
- package/lib/client/components/format.js.map +1 -0
- package/lib/client/components/source-refs.js +78 -0
- package/lib/client/components/source-refs.js.map +1 -0
- package/lib/client/create-session.js +51 -0
- package/lib/client/create-session.js.map +1 -0
- package/lib/client/index.js +166 -35
- package/lib/client/index.js.map +1 -1
- package/lib/client/project-session-state.js +78 -0
- package/lib/client/project-session-state.js.map +1 -0
- package/lib/client/session-admission.js +38 -0
- package/lib/client/session-admission.js.map +1 -0
- package/lib/client/session-assets.js +52 -0
- package/lib/client/session-assets.js.map +1 -0
- package/lib/client/session-sources.js +61 -0
- package/lib/client/session-sources.js.map +1 -0
- package/lib/client/settlement.js +24 -0
- package/lib/client/settlement.js.map +1 -0
- package/lib/client/styles.js +450 -43
- package/lib/client/styles.js.map +1 -1
- package/lib/client/tool-presentation.js +102 -0
- package/lib/client/tool-presentation.js.map +1 -0
- package/lib/client/turn-sources.js +98 -0
- package/lib/client/turn-sources.js.map +1 -0
- package/lib/client/types.js +15 -0
- package/lib/client/types.js.map +1 -0
- package/lib/client/use-session-ui-state.js +68 -0
- package/lib/client/use-session-ui-state.js.map +1 -0
- package/lib/client/use-transcript-view.js +44 -0
- package/lib/client/use-transcript-view.js.map +1 -0
- package/lib/client/wait-for.js +49 -0
- package/lib/client/wait-for.js.map +1 -0
- package/lib/client.js +12551 -918
- package/lib/client.js.map +1 -1
- package/lib/config.js +226 -58
- package/lib/config.js.map +1 -1
- package/lib/entry-redirect.js +44 -0
- package/lib/entry-redirect.js.map +1 -0
- package/lib/host-route.js +25 -1
- package/lib/host-route.js.map +1 -1
- package/lib/index.js +354 -72
- package/lib/index.js.map +1 -1
- package/lib/launch-token.js +42 -0
- package/lib/launch-token.js.map +1 -0
- package/lib/lockdown-policy.js +20 -0
- package/lib/lockdown-policy.js.map +1 -0
- package/lib/provenance/collector.js +40 -0
- package/lib/provenance/collector.js.map +1 -0
- package/lib/provenance/connectors.js +147 -0
- package/lib/provenance/connectors.js.map +1 -0
- package/lib/provenance/dedupe.js +89 -0
- package/lib/provenance/dedupe.js.map +1 -0
- package/lib/provenance/extractors.js +340 -0
- package/lib/provenance/extractors.js.map +1 -0
- package/lib/provenance/file-preview.js +39 -0
- package/lib/provenance/file-preview.js.map +1 -0
- package/lib/provenance/host-store.js +387 -0
- package/lib/provenance/host-store.js.map +1 -0
- package/lib/provenance/index.js +9 -0
- package/lib/provenance/index.js.map +1 -0
- package/lib/provenance/normalize.js +128 -0
- package/lib/provenance/normalize.js.map +1 -0
- package/lib/provenance/reported.js +53 -0
- package/lib/provenance/reported.js.map +1 -0
- package/lib/provenance/types.js +2 -0
- package/lib/provenance/types.js.map +1 -0
- package/lib/resolve-config.js +283 -10
- package/lib/resolve-config.js.map +1 -1
- package/lib/secure-session.js +212 -0
- package/lib/secure-session.js.map +1 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +784 -0
- package/lib/typert.remote-client.d.ts +40 -0
- package/lib/typert.remote-client.js +597 -0
- package/lib/types/accounts/index.d.ts +2 -0
- package/lib/types/accounts/store.d.ts +121 -0
- package/lib/types/attestation.d.ts +17 -0
- package/lib/types/cli.d.ts +9 -0
- package/lib/types/client/QaAccountsController.d.ts +95 -0
- package/lib/types/client/QaConfigController.d.ts +35 -3
- package/lib/types/client/QaSessionController.d.ts +146 -20
- package/lib/types/client/QaSurface.d.ts +11 -5
- package/lib/types/client/QaTranscriptAdapter.d.ts +13 -3
- package/lib/types/client/attestation.d.ts +25 -0
- package/lib/types/client/chat-index.d.ts +28 -0
- package/lib/types/client/components/Markdown.d.ts +17 -3
- package/lib/types/client/components/QaAgentsDrawer.d.ts +25 -0
- package/lib/types/client/components/QaAuthGate.d.ts +15 -0
- package/lib/types/client/components/QaChangelog.d.ts +21 -0
- package/lib/types/client/components/QaComposer.d.ts +12 -2
- package/lib/types/client/components/QaMessage.d.ts +22 -2
- package/lib/types/client/components/QaSidebar.d.ts +69 -0
- package/lib/types/client/components/QaSourcesDrawer.d.ts +20 -0
- package/lib/types/client/components/QaTurnRail.d.ts +59 -0
- package/lib/types/client/components/QaWelcomeNotice.d.ts +27 -0
- package/lib/types/client/components/QaWidthHandle.d.ts +36 -0
- package/lib/types/client/components/QaWorkGroup.d.ts +19 -0
- package/lib/types/client/components/VariantSwitcher.d.ts +19 -0
- package/lib/types/client/components/format.d.ts +9 -0
- package/lib/types/client/components/source-refs.d.ts +16 -0
- package/lib/types/client/create-session.d.ts +16 -0
- package/lib/types/client/index.d.ts +2 -5
- package/lib/types/client/project-session-state.d.ts +30 -0
- package/lib/types/client/session-admission.d.ts +23 -0
- package/lib/types/client/session-assets.d.ts +20 -0
- package/lib/types/client/session-sources.d.ts +28 -0
- package/lib/types/client/settlement.d.ts +6 -0
- package/lib/types/client/tool-presentation.d.ts +6 -0
- package/lib/types/client/turn-sources.d.ts +14 -0
- package/lib/types/client/types.d.ts +93 -0
- package/lib/types/client/use-session-ui-state.d.ts +35 -0
- package/lib/types/client/use-transcript-view.d.ts +20 -0
- package/lib/types/client/wait-for.d.ts +11 -0
- package/lib/types/entry-redirect.d.ts +20 -0
- package/lib/types/host-route.d.ts +14 -1
- package/lib/types/index.d.ts +67 -7
- package/lib/types/launch-token.d.ts +21 -0
- package/lib/types/lockdown-policy.d.ts +18 -0
- package/lib/types/provenance/collector.d.ts +26 -0
- package/lib/types/provenance/connectors.d.ts +11 -0
- package/lib/types/provenance/dedupe.d.ts +6 -0
- package/lib/types/provenance/extractors.d.ts +14 -0
- package/lib/types/provenance/file-preview.d.ts +10 -0
- package/lib/types/provenance/host-store.d.ts +54 -0
- package/lib/types/provenance/index.d.ts +10 -0
- package/lib/types/provenance/normalize.d.ts +13 -0
- package/lib/types/provenance/reported.d.ts +5 -0
- package/lib/types/provenance/types.d.ts +78 -0
- package/lib/types/secure-session.d.ts +31 -0
- package/lib/types/types.d.ts +304 -1
- package/package.json +70 -27
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/accounts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,eAAe,EACf,uBAAuB,GAGxB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { createHmac, randomBytes, randomUUID, scryptSync, timingSafeEqual, } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export class QaAccountsError extends Error {
|
|
5
|
+
reason;
|
|
6
|
+
constructor(reason, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
this.name = "QaAccountsError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const SCRYPT_KEY_LENGTH = 32;
|
|
13
|
+
const SECRET_BYTES = 32;
|
|
14
|
+
const MIN_PASSWORD_LENGTH = 8;
|
|
15
|
+
const MAX_DISPLAY_NAME_LENGTH = 100;
|
|
16
|
+
const MAX_CLAIM_BATCH = 50;
|
|
17
|
+
const MAX_SESSION_ID_LENGTH = 200;
|
|
18
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
|
|
19
|
+
/** The accounts file lives next to the DSH home the launcher exports. */
|
|
20
|
+
export function defaultAccountsFilePath() {
|
|
21
|
+
const home = process.env.DSH_HOME?.trim();
|
|
22
|
+
return path.join(home !== undefined && home !== "" ? home : process.cwd(), "qa-accounts.json");
|
|
23
|
+
}
|
|
24
|
+
function toPublic(user) {
|
|
25
|
+
return {
|
|
26
|
+
id: user.id,
|
|
27
|
+
email: user.email,
|
|
28
|
+
displayName: user.displayName,
|
|
29
|
+
role: user.role,
|
|
30
|
+
createdAt: user.createdAt,
|
|
31
|
+
lastLoginAt: user.lastLoginAt,
|
|
32
|
+
disabled: user.disabled === true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function hashPassword(password) {
|
|
36
|
+
const salt = randomBytes(16);
|
|
37
|
+
const hash = scryptSync(password, salt, SCRYPT_KEY_LENGTH);
|
|
38
|
+
return { salt: salt.toString("hex"), hash: hash.toString("hex") };
|
|
39
|
+
}
|
|
40
|
+
function passwordMatches(password, stored) {
|
|
41
|
+
const expected = Buffer.from(stored.hash, "hex");
|
|
42
|
+
const actual = scryptSync(password, Buffer.from(stored.salt, "hex"), expected.length);
|
|
43
|
+
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
44
|
+
}
|
|
45
|
+
function base64Url(value) {
|
|
46
|
+
return Buffer.from(value).toString("base64url");
|
|
47
|
+
}
|
|
48
|
+
function stampOf(stat) {
|
|
49
|
+
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Host authority for QA accounts: the file-backed user list, the session
|
|
53
|
+
* ownership map and the HMAC account tokens. All mutations are synchronous
|
|
54
|
+
* and persisted atomically (temp file + rename); LAN scale keeps this trivial.
|
|
55
|
+
*/
|
|
56
|
+
export class QaAccounts {
|
|
57
|
+
filePath;
|
|
58
|
+
file;
|
|
59
|
+
authAttempts = [];
|
|
60
|
+
maxAuthAttemptsPerMinute;
|
|
61
|
+
sessionTtlDays;
|
|
62
|
+
allowRegistration;
|
|
63
|
+
/** mtime+size of the file as of the last load; the external-change probe. */
|
|
64
|
+
lastLoadStamp = { mtimeMs: Number.NEGATIVE_INFINITY, size: -1 };
|
|
65
|
+
constructor(filePath = defaultAccountsFilePath(), options) {
|
|
66
|
+
this.filePath = filePath;
|
|
67
|
+
this.sessionTtlDays = options.sessionTtlDays;
|
|
68
|
+
this.allowRegistration = options.allowRegistration;
|
|
69
|
+
this.maxAuthAttemptsPerMinute = options.maxAuthAttemptsPerMinute ?? 30;
|
|
70
|
+
this.file = this.load();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The qa-accounts CLI runs in its own process against the same file. Re-read
|
|
74
|
+
* it whenever it changed on disk, before every read or read-modify-write:
|
|
75
|
+
* without this a running Host would keep honoring tokens the CLI revoked
|
|
76
|
+
* and its next save would resurrect the CLI's change from a stale snapshot.
|
|
77
|
+
* One file, one writer at a time remains the deployment's discipline; LAN
|
|
78
|
+
* scale keeps the stat per operation trivial.
|
|
79
|
+
*/
|
|
80
|
+
reloadIfChanged() {
|
|
81
|
+
let stat;
|
|
82
|
+
try {
|
|
83
|
+
stat = statSync(this.filePath);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// The file vanished between operations (never by our hand): keep
|
|
87
|
+
// serving the in-memory state instead of resetting every account.
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (stat.mtimeMs === this.lastLoadStamp.mtimeMs &&
|
|
91
|
+
stat.size === this.lastLoadStamp.size) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.file = this.load();
|
|
95
|
+
}
|
|
96
|
+
load() {
|
|
97
|
+
let raw;
|
|
98
|
+
let stat;
|
|
99
|
+
try {
|
|
100
|
+
raw = readFileSync(this.filePath, "utf8");
|
|
101
|
+
stat = statSync(this.filePath);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// Only a missing file means "first run": anything else (permissions,
|
|
105
|
+
// a directory in the way, I/O failure) must surface, or the store
|
|
106
|
+
// would silently reset every account it cannot read.
|
|
107
|
+
if (error.code !== "ENOENT")
|
|
108
|
+
throw error;
|
|
109
|
+
const created = {
|
|
110
|
+
version: 1,
|
|
111
|
+
secret: base64Url(randomBytes(SECRET_BYTES)),
|
|
112
|
+
users: [],
|
|
113
|
+
ownership: {},
|
|
114
|
+
};
|
|
115
|
+
this.persist(created);
|
|
116
|
+
return created;
|
|
117
|
+
}
|
|
118
|
+
const parsed = JSON.parse(raw);
|
|
119
|
+
if (typeof parsed !== "object" ||
|
|
120
|
+
parsed === null ||
|
|
121
|
+
parsed.version !== 1 ||
|
|
122
|
+
typeof parsed.secret !== "string" ||
|
|
123
|
+
!Array.isArray(parsed.users)) {
|
|
124
|
+
throw new Error(`qa-accounts: ${this.filePath} is not a recognizable accounts file; refusing to overwrite it`);
|
|
125
|
+
}
|
|
126
|
+
const file = parsed;
|
|
127
|
+
if (stat !== undefined)
|
|
128
|
+
this.lastLoadStamp = stampOf(stat);
|
|
129
|
+
return {
|
|
130
|
+
version: 1,
|
|
131
|
+
secret: file.secret,
|
|
132
|
+
users: file.users,
|
|
133
|
+
ownership: file.ownership ?? {},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
persist(file) {
|
|
137
|
+
mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
138
|
+
// A per-process temp name: the Host and the CLI never race on one file.
|
|
139
|
+
const temp = `${this.filePath}.${process.pid}.tmp`;
|
|
140
|
+
writeFileSync(temp, `${JSON.stringify(file, null, 2)}\n`, "utf8");
|
|
141
|
+
renameSync(temp, this.filePath);
|
|
142
|
+
this.lastLoadStamp = stampOf(statSync(this.filePath));
|
|
143
|
+
}
|
|
144
|
+
save() {
|
|
145
|
+
this.persist(this.file);
|
|
146
|
+
}
|
|
147
|
+
pruneAuthAttempts() {
|
|
148
|
+
const cutoff = Date.now() - 60_000;
|
|
149
|
+
let stale = 0;
|
|
150
|
+
for (const at of this.authAttempts) {
|
|
151
|
+
if (at >= cutoff)
|
|
152
|
+
break;
|
|
153
|
+
stale += 1;
|
|
154
|
+
}
|
|
155
|
+
if (stale > 0)
|
|
156
|
+
this.authAttempts.splice(0, stale);
|
|
157
|
+
}
|
|
158
|
+
assertAuthBudget() {
|
|
159
|
+
this.pruneAuthAttempts();
|
|
160
|
+
if (this.authAttempts.length >= this.maxAuthAttemptsPerMinute) {
|
|
161
|
+
throw new QaAccountsError("rate-limited", "too many authentication attempts; retry in a minute");
|
|
162
|
+
}
|
|
163
|
+
this.authAttempts.push(Date.now());
|
|
164
|
+
}
|
|
165
|
+
userByEmail(email) {
|
|
166
|
+
return this.file.users.find((user) => user.email === email);
|
|
167
|
+
}
|
|
168
|
+
register(email, password, displayName) {
|
|
169
|
+
this.reloadIfChanged();
|
|
170
|
+
this.assertAuthBudget();
|
|
171
|
+
if (!this.allowRegistration) {
|
|
172
|
+
throw new QaAccountsError("registration-disabled", "self-registration is disabled on this deployment");
|
|
173
|
+
}
|
|
174
|
+
const normalized = email.trim().toLowerCase();
|
|
175
|
+
if (!EMAIL_PATTERN.test(normalized) || normalized.length > 254) {
|
|
176
|
+
throw new QaAccountsError("invalid-email", "email is not a usable address");
|
|
177
|
+
}
|
|
178
|
+
if (password.length < MIN_PASSWORD_LENGTH || password.length > 200) {
|
|
179
|
+
throw new QaAccountsError("weak-password", `password must be ${MIN_PASSWORD_LENGTH} to 200 characters`);
|
|
180
|
+
}
|
|
181
|
+
const name = (displayName ?? "").trim();
|
|
182
|
+
if (name.length > MAX_DISPLAY_NAME_LENGTH) {
|
|
183
|
+
throw new QaAccountsError("invalid-display-name", `display name must be at most ${MAX_DISPLAY_NAME_LENGTH} characters`);
|
|
184
|
+
}
|
|
185
|
+
if (this.userByEmail(normalized) !== undefined) {
|
|
186
|
+
throw new QaAccountsError("email-taken", "this email is already registered");
|
|
187
|
+
}
|
|
188
|
+
// The first account boots the deployment's admin; later ones are users.
|
|
189
|
+
const role = this.file.users.length === 0 ? "admin" : "user";
|
|
190
|
+
const user = {
|
|
191
|
+
id: randomUUID(),
|
|
192
|
+
email: normalized,
|
|
193
|
+
displayName: name === "" ? (normalized.split("@")[0] ?? normalized) : name,
|
|
194
|
+
role,
|
|
195
|
+
passwordHash: hashPassword(password),
|
|
196
|
+
createdAt: new Date().toISOString(),
|
|
197
|
+
lastLoginAt: new Date().toISOString(),
|
|
198
|
+
tokenVersion: 0,
|
|
199
|
+
};
|
|
200
|
+
this.file = { ...this.file, users: [...this.file.users, user] };
|
|
201
|
+
this.save();
|
|
202
|
+
return { token: this.mintToken(user.id), user: toPublic(user) };
|
|
203
|
+
}
|
|
204
|
+
login(email, password) {
|
|
205
|
+
this.reloadIfChanged();
|
|
206
|
+
this.assertAuthBudget();
|
|
207
|
+
const user = this.userByEmail(email.trim().toLowerCase());
|
|
208
|
+
// One message for unknown email and wrong password, both scrypt-checked
|
|
209
|
+
// in the known-user path so the timing does not enumerate addresses.
|
|
210
|
+
if (user === undefined) {
|
|
211
|
+
passwordMatches(password, hashPassword(password));
|
|
212
|
+
throw new QaAccountsError("invalid-credentials", "email or password is incorrect");
|
|
213
|
+
}
|
|
214
|
+
if (!passwordMatches(password, user.passwordHash)) {
|
|
215
|
+
throw new QaAccountsError("invalid-credentials", "email or password is incorrect");
|
|
216
|
+
}
|
|
217
|
+
if (user.disabled === true) {
|
|
218
|
+
throw new QaAccountsError("account-disabled", "this account has been disabled by the operator");
|
|
219
|
+
}
|
|
220
|
+
user.lastLoginAt = new Date().toISOString();
|
|
221
|
+
this.save();
|
|
222
|
+
return { token: this.mintToken(user.id), user: toPublic(user) };
|
|
223
|
+
}
|
|
224
|
+
whoami(token) {
|
|
225
|
+
this.reloadIfChanged();
|
|
226
|
+
const userId = this.verifyToken(token);
|
|
227
|
+
if (userId === null)
|
|
228
|
+
return { authenticated: false };
|
|
229
|
+
const user = this.file.users.find((candidate) => candidate.id === userId);
|
|
230
|
+
return user === undefined || user.disabled === true
|
|
231
|
+
? { authenticated: false }
|
|
232
|
+
: { authenticated: true, user: toPublic(user) };
|
|
233
|
+
}
|
|
234
|
+
/** Resolve a token to its user, or null for absent/invalid/expired ones. */
|
|
235
|
+
verifyToken(token) {
|
|
236
|
+
if (typeof token !== "string")
|
|
237
|
+
return null;
|
|
238
|
+
const parts = token.split(".");
|
|
239
|
+
if (parts.length !== 3 || parts[0] !== "v1")
|
|
240
|
+
return null;
|
|
241
|
+
const payload = parts[1];
|
|
242
|
+
const signature = parts[2];
|
|
243
|
+
if (payload === undefined || signature === undefined)
|
|
244
|
+
return null;
|
|
245
|
+
let expected;
|
|
246
|
+
let decoded;
|
|
247
|
+
try {
|
|
248
|
+
expected = createHmac("sha256", this.file.secret)
|
|
249
|
+
.update(payload)
|
|
250
|
+
.digest();
|
|
251
|
+
const expectedSignature = base64Url(expected);
|
|
252
|
+
if (signature.length !== expectedSignature.length ||
|
|
253
|
+
!timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (typeof decoded.uid !== "string" ||
|
|
262
|
+
typeof decoded.exp !== "number" ||
|
|
263
|
+
decoded.exp < Date.now()) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
// The account's current token version must match the one burned into the
|
|
267
|
+
// token: bumping it (disable/revoke) invalidates every issued token.
|
|
268
|
+
const user = this.file.users.find((candidate) => candidate.id === decoded.uid);
|
|
269
|
+
const version = typeof decoded.ver === "number" ? decoded.ver : undefined;
|
|
270
|
+
if (user === undefined || version !== (user.tokenVersion ?? 0)) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return decoded.uid;
|
|
274
|
+
}
|
|
275
|
+
mintToken(userId) {
|
|
276
|
+
const user = this.file.users.find((candidate) => candidate.id === userId);
|
|
277
|
+
const payload = base64Url(JSON.stringify({
|
|
278
|
+
uid: userId,
|
|
279
|
+
exp: Date.now() + this.sessionTtlDays * 86_400_000,
|
|
280
|
+
ver: user?.tokenVersion ?? 0,
|
|
281
|
+
}));
|
|
282
|
+
const signature = base64Url(createHmac("sha256", this.file.secret).update(payload).digest());
|
|
283
|
+
return `v1.${payload}.${signature}`;
|
|
284
|
+
}
|
|
285
|
+
/** The account behind a token, or an auth-required refusal. */
|
|
286
|
+
requireUser(token) {
|
|
287
|
+
const userId = this.verifyToken(token);
|
|
288
|
+
const user = userId === null
|
|
289
|
+
? undefined
|
|
290
|
+
: this.file.users.find((candidate) => candidate.id === userId);
|
|
291
|
+
if (user === undefined || user.disabled === true) {
|
|
292
|
+
throw new QaAccountsError("auth-required", "a valid QA account token is required");
|
|
293
|
+
}
|
|
294
|
+
return user;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Ownership gate for one session: claim unowned sessions for the requesting
|
|
298
|
+
* user (first come, first served — the pre-accounts migration path), refuse
|
|
299
|
+
* foreign sessions unless the caller administers the deployment.
|
|
300
|
+
*/
|
|
301
|
+
ensureSessionAccess(token, sessionId) {
|
|
302
|
+
this.reloadIfChanged();
|
|
303
|
+
const user = this.requireUser(token);
|
|
304
|
+
const owner = this.file.ownership[sessionId];
|
|
305
|
+
if (owner === undefined) {
|
|
306
|
+
this.file = {
|
|
307
|
+
...this.file,
|
|
308
|
+
ownership: {
|
|
309
|
+
...this.file.ownership,
|
|
310
|
+
[sessionId]: { userId: user.id, claimedAt: new Date().toISOString() },
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
this.save();
|
|
314
|
+
return toPublic(user);
|
|
315
|
+
}
|
|
316
|
+
if (owner.userId !== user.id && user.role !== "admin") {
|
|
317
|
+
throw new QaAccountsError("session-owned-elsewhere", "this session belongs to another QA user");
|
|
318
|
+
}
|
|
319
|
+
return toPublic(user);
|
|
320
|
+
}
|
|
321
|
+
/** Bulk-claim a browser's local chat index; foreign ids come back as conflicts. */
|
|
322
|
+
claimSessions(token, sessionIds) {
|
|
323
|
+
this.reloadIfChanged();
|
|
324
|
+
const user = this.requireUser(token);
|
|
325
|
+
if (sessionIds.length > MAX_CLAIM_BATCH) {
|
|
326
|
+
throw new QaAccountsError("auth-required", "claim batch is too large");
|
|
327
|
+
}
|
|
328
|
+
let claimed = 0;
|
|
329
|
+
const conflicts = [];
|
|
330
|
+
const ownership = { ...this.file.ownership };
|
|
331
|
+
for (const raw of sessionIds) {
|
|
332
|
+
const sessionId = typeof raw === "string" ? raw.trim() : "";
|
|
333
|
+
if (sessionId === "" || sessionId.length > MAX_SESSION_ID_LENGTH)
|
|
334
|
+
continue;
|
|
335
|
+
const owner = ownership[sessionId];
|
|
336
|
+
if (owner === undefined) {
|
|
337
|
+
ownership[sessionId] = {
|
|
338
|
+
userId: user.id,
|
|
339
|
+
claimedAt: new Date().toISOString(),
|
|
340
|
+
};
|
|
341
|
+
claimed += 1;
|
|
342
|
+
}
|
|
343
|
+
else if (owner.userId !== user.id) {
|
|
344
|
+
conflicts.push(sessionId);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (claimed > 0) {
|
|
348
|
+
this.file = { ...this.file, ownership };
|
|
349
|
+
this.save();
|
|
350
|
+
}
|
|
351
|
+
return { claimed, conflicts };
|
|
352
|
+
}
|
|
353
|
+
/** Session ids owned by the token's user, most recently claimed first. */
|
|
354
|
+
ownedSessionIds(token) {
|
|
355
|
+
this.reloadIfChanged();
|
|
356
|
+
const user = this.requireUser(token);
|
|
357
|
+
return Object.entries(this.file.ownership)
|
|
358
|
+
.filter(([, owner]) => owner.userId === user.id)
|
|
359
|
+
.sort((left, right) => right[1].claimedAt.localeCompare(left[1].claimedAt))
|
|
360
|
+
.map(([sessionId]) => sessionId);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Every ownership entry with the owner's display name resolved at read
|
|
364
|
+
* time, oldest claim first. Admin-only: this is the cross-user view the
|
|
365
|
+
* admin sidebar groups chats by; ordinary accounts get a dedicated refusal.
|
|
366
|
+
*/
|
|
367
|
+
listOwnership(token) {
|
|
368
|
+
const user = this.requireUser(token);
|
|
369
|
+
if (user.role !== "admin") {
|
|
370
|
+
throw new QaAccountsError("admin-required", "listing chat ownership requires an admin account");
|
|
371
|
+
}
|
|
372
|
+
const names = new Map(this.file.users.map((candidate) => [candidate.id, candidate.displayName]));
|
|
373
|
+
return Object.entries(this.file.ownership)
|
|
374
|
+
.map(([sessionId, owner]) => ({
|
|
375
|
+
sessionId,
|
|
376
|
+
userId: owner.userId,
|
|
377
|
+
displayName: names.get(owner.userId) ?? owner.userId,
|
|
378
|
+
claimedAt: owner.claimedAt,
|
|
379
|
+
}))
|
|
380
|
+
.sort((left, right) => left.claimedAt.localeCompare(right.claimedAt));
|
|
381
|
+
}
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Operator management surface (the qa-accounts CLI; never browser-callable).
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
/** Every account, file order; for `qa-accounts list`. */
|
|
386
|
+
listUsers() {
|
|
387
|
+
this.reloadIfChanged();
|
|
388
|
+
return this.file.users.map((user) => ({
|
|
389
|
+
email: user.email,
|
|
390
|
+
displayName: user.displayName,
|
|
391
|
+
role: user.role,
|
|
392
|
+
disabled: user.disabled === true,
|
|
393
|
+
createdAt: user.createdAt,
|
|
394
|
+
lastLoginAt: user.lastLoginAt,
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
/** Create an account outside the self-registration gate. */
|
|
398
|
+
addUser(email, password, options = {}) {
|
|
399
|
+
this.reloadIfChanged();
|
|
400
|
+
this.assertAuthBudget();
|
|
401
|
+
const normalized = email.trim().toLowerCase();
|
|
402
|
+
if (!EMAIL_PATTERN.test(normalized) || normalized.length > 254) {
|
|
403
|
+
throw new QaAccountsError("invalid-email", "email is not a usable address");
|
|
404
|
+
}
|
|
405
|
+
if (password.length < MIN_PASSWORD_LENGTH || password.length > 200) {
|
|
406
|
+
throw new QaAccountsError("weak-password", `password must be ${MIN_PASSWORD_LENGTH} to 200 characters`);
|
|
407
|
+
}
|
|
408
|
+
const name = (options.displayName ?? "").trim();
|
|
409
|
+
if (name.length > MAX_DISPLAY_NAME_LENGTH) {
|
|
410
|
+
throw new QaAccountsError("invalid-display-name", `display name must be at most ${MAX_DISPLAY_NAME_LENGTH} characters`);
|
|
411
|
+
}
|
|
412
|
+
if (options.role !== undefined &&
|
|
413
|
+
options.role !== "admin" &&
|
|
414
|
+
options.role !== "user") {
|
|
415
|
+
throw new QaAccountsError("invalid-role", "role must be admin or user");
|
|
416
|
+
}
|
|
417
|
+
if (this.userByEmail(normalized) !== undefined) {
|
|
418
|
+
throw new QaAccountsError("email-taken", "this email is already registered");
|
|
419
|
+
}
|
|
420
|
+
const user = {
|
|
421
|
+
id: randomUUID(),
|
|
422
|
+
email: normalized,
|
|
423
|
+
displayName: name === "" ? (normalized.split("@")[0] ?? normalized) : name,
|
|
424
|
+
role: options.role ?? (this.file.users.length === 0 ? "admin" : "user"),
|
|
425
|
+
passwordHash: hashPassword(password),
|
|
426
|
+
createdAt: new Date().toISOString(),
|
|
427
|
+
lastLoginAt: null,
|
|
428
|
+
tokenVersion: 0,
|
|
429
|
+
};
|
|
430
|
+
this.file = { ...this.file, users: [...this.file.users, user] };
|
|
431
|
+
this.save();
|
|
432
|
+
return toPublic(user);
|
|
433
|
+
}
|
|
434
|
+
editUser(email, edit) {
|
|
435
|
+
this.reloadIfChanged();
|
|
436
|
+
const normalized = email.trim().toLowerCase();
|
|
437
|
+
const index = this.file.users.findIndex((user) => user.email === normalized);
|
|
438
|
+
if (index === -1) {
|
|
439
|
+
throw new QaAccountsError("invalid-credentials", "no such account");
|
|
440
|
+
}
|
|
441
|
+
const updated = edit(this.file.users[index]);
|
|
442
|
+
const users = [...this.file.users];
|
|
443
|
+
users[index] = updated;
|
|
444
|
+
this.file = { ...this.file, users };
|
|
445
|
+
this.save();
|
|
446
|
+
return updated;
|
|
447
|
+
}
|
|
448
|
+
/** Grant or revoke the admin role. */
|
|
449
|
+
setUserRole(email, role) {
|
|
450
|
+
return toPublic(this.editUser(email, (user) => ({ ...user, role })));
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Disable an account: future logins are refused and every live token is
|
|
454
|
+
* invalidated (the token version bumps). `enable` does not resurrect old
|
|
455
|
+
* tokens — the account signs in again.
|
|
456
|
+
*/
|
|
457
|
+
setUserDisabled(email, disabled) {
|
|
458
|
+
return toPublic(this.editUser(email, (user) => {
|
|
459
|
+
const next = user;
|
|
460
|
+
if (disabled) {
|
|
461
|
+
next.disabled = true;
|
|
462
|
+
next.tokenVersion = (next.tokenVersion ?? 0) + 1;
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
delete next.disabled;
|
|
466
|
+
}
|
|
467
|
+
return next;
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
/** Invalidate every token of the account (password-leak response). */
|
|
471
|
+
revokeTokens(email) {
|
|
472
|
+
return toPublic(this.editUser(email, (user) => ({
|
|
473
|
+
...user,
|
|
474
|
+
tokenVersion: (user.tokenVersion ?? 0) + 1,
|
|
475
|
+
})));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/accounts/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Host rejection whose wire message carries the coarse reason marker. */
|
|
2
|
+
export class QaAttestationError extends Error {
|
|
3
|
+
reason;
|
|
4
|
+
constructor(reason, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.reason = reason;
|
|
7
|
+
this.name = "QaAttestationError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=attestation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attestation.js","sourceRoot":"","sources":["../src/attestation.ts"],"names":[],"mappings":"AAqBA,0EAA0E;AAC1E,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAEhC,MAAM;IADjB,YACW,MAA2B,EACpC,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;sBAHN,MAAM;QAIf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF"}
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { QaAccounts, QaAccountsError, defaultAccountsFilePath, } from "./accounts/store.js";
|
|
5
|
+
const defaultIo = {
|
|
6
|
+
out: (line) => process.stdout.write(`${line}\n`),
|
|
7
|
+
err: (line) => process.stderr.write(`${line}\n`),
|
|
8
|
+
};
|
|
9
|
+
const USAGE = `Usage:
|
|
10
|
+
qa-accounts [--file <path>] list
|
|
11
|
+
qa-accounts [--file <path>] add <email> --password-stdin [--name <name>] [--role admin|user]
|
|
12
|
+
qa-accounts [--file <path>] set-role <email> <admin|user>
|
|
13
|
+
qa-accounts [--file <path>] disable <email> # blocks logins and revokes live tokens
|
|
14
|
+
qa-accounts [--file <path>] enable <email>
|
|
15
|
+
qa-accounts [--file <path>] revoke <email> # invalidates every issued token
|
|
16
|
+
|
|
17
|
+
The accounts file defaults to \\$DSH_HOME/qa-accounts.json. Passwords are read
|
|
18
|
+
from stdin (one line) so they never land in shell history.`;
|
|
19
|
+
function readPasswordStdin() {
|
|
20
|
+
let password;
|
|
21
|
+
try {
|
|
22
|
+
password = readFileSync(0, "utf8");
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new Error(`could not read the password from stdin: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
26
|
+
}
|
|
27
|
+
return password.replace(/\r?\n$/u, "");
|
|
28
|
+
}
|
|
29
|
+
/** Execute one management command; returns the process exit code. */
|
|
30
|
+
export function run(argv, io, readPassword = readPasswordStdin) {
|
|
31
|
+
const positional = [];
|
|
32
|
+
const options = new Map();
|
|
33
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
34
|
+
const arg = argv[index];
|
|
35
|
+
if (arg === "--password-stdin") {
|
|
36
|
+
options.set("password-stdin", "true");
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (arg === "--file" || arg === "--name" || arg === "--role") {
|
|
40
|
+
const value = argv[index + 1];
|
|
41
|
+
if (value === undefined || value.startsWith("--")) {
|
|
42
|
+
throw new Error(`${arg} requires a value`);
|
|
43
|
+
}
|
|
44
|
+
options.set(arg.slice(2), value);
|
|
45
|
+
index += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
positional.push(arg);
|
|
49
|
+
}
|
|
50
|
+
const file = options.get("file") ?? defaultAccountsFilePath();
|
|
51
|
+
const [command, first, second] = positional;
|
|
52
|
+
if (command === undefined || command === "help" || command === "--help") {
|
|
53
|
+
io.out(USAGE);
|
|
54
|
+
return command === undefined ? 1 : 0;
|
|
55
|
+
}
|
|
56
|
+
// Management options that only shape minting behavior; the CLI never mints.
|
|
57
|
+
const accounts = new QaAccounts(file, {
|
|
58
|
+
sessionTtlDays: 30,
|
|
59
|
+
allowRegistration: false,
|
|
60
|
+
});
|
|
61
|
+
const requireArgs = (count) => {
|
|
62
|
+
const given = [first, second].filter((value) => value !== undefined).length;
|
|
63
|
+
if (given < count) {
|
|
64
|
+
throw new Error(`the ${command} command requires ${count} argument(s)`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
switch (command) {
|
|
68
|
+
case "list": {
|
|
69
|
+
const users = accounts.listUsers();
|
|
70
|
+
if (users.length === 0) {
|
|
71
|
+
io.out("no accounts yet");
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
for (const user of users) {
|
|
75
|
+
io.out(`${user.email}\t${user.displayName}\t${user.role}` +
|
|
76
|
+
`${user.disabled ? "\tdisabled" : ""}` +
|
|
77
|
+
`${user.lastLoginAt === null ? "" : `\tlast login ${user.lastLoginAt}`}`);
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
case "add": {
|
|
82
|
+
requireArgs(1);
|
|
83
|
+
if (!options.has("password-stdin")) {
|
|
84
|
+
throw new Error("the add command requires --password-stdin");
|
|
85
|
+
}
|
|
86
|
+
const role = options.get("role");
|
|
87
|
+
if (role !== undefined && role !== "admin" && role !== "user") {
|
|
88
|
+
throw new Error("--role must be admin or user");
|
|
89
|
+
}
|
|
90
|
+
const name = options.get("name");
|
|
91
|
+
const user = accounts.addUser(first, readPassword(), {
|
|
92
|
+
...(name === undefined ? {} : { displayName: name }),
|
|
93
|
+
...(role === undefined ? {} : { role: role }),
|
|
94
|
+
});
|
|
95
|
+
io.out(`added ${user.email} (${user.role})`);
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
case "set-role": {
|
|
99
|
+
requireArgs(2);
|
|
100
|
+
if (second !== "admin" && second !== "user") {
|
|
101
|
+
throw new Error("role must be admin or user");
|
|
102
|
+
}
|
|
103
|
+
const user = accounts.setUserRole(first, second);
|
|
104
|
+
io.out(`${user.email} is now ${user.role}`);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
case "disable": {
|
|
108
|
+
requireArgs(1);
|
|
109
|
+
const user = accounts.setUserDisabled(first, true);
|
|
110
|
+
io.out(`${user.email} disabled; live tokens revoked`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
case "enable": {
|
|
114
|
+
requireArgs(1);
|
|
115
|
+
const user = accounts.setUserDisabled(first, false);
|
|
116
|
+
io.out(`${user.email} enabled`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
case "revoke": {
|
|
120
|
+
requireArgs(1);
|
|
121
|
+
const user = accounts.revokeTokens(first);
|
|
122
|
+
io.out(`${user.email} tokens revoked`);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
throw new Error(`unknown command ${JSON.stringify(command)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function main(argv, io = defaultIo, readPassword = readPasswordStdin) {
|
|
130
|
+
try {
|
|
131
|
+
return run(argv, io, readPassword);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
io.err(`qa-accounts: ${error instanceof QaAccountsError ? `${error.message} (reason: ${error.reason})` : error instanceof Error ? error.message : String(error)}`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/* v8 ignore next */
|
|
139
|
+
if (process.argv[1] !== undefined &&
|
|
140
|
+
import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
141
|
+
process.exitCode = main(process.argv.slice(2));
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=cli.js.map
|