shadok-ai 0.15.37 → 0.15.38
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 +25 -6
- package/dist/accounts.d.ts +13 -3
- package/dist/accounts.js +8 -4
- package/dist/accounts.js.map +1 -1
- package/dist/admin-requests.d.ts +78 -0
- package/dist/admin-requests.js +137 -0
- package/dist/admin-requests.js.map +1 -0
- package/dist/security-events.d.ts +76 -0
- package/dist/security-events.js +115 -0
- package/dist/security-events.js.map +1 -0
- package/dist/server.js +302 -10
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/public/index.html +226 -22
package/README.md
CHANGED
|
@@ -392,7 +392,7 @@ know before opening it up:
|
|
|
392
392
|
- **That password is the `admin` account.** With it set, the login page gains a
|
|
393
393
|
user field — leave it blank to sign in as the admin. An admin can then invite
|
|
394
394
|
other people from the **Users** panel: each invitation is a single-use link,
|
|
395
|
-
valid
|
|
395
|
+
valid 48 hours, on which the invitee chooses their own password, so the admin
|
|
396
396
|
never learns it — and redeeming the link signs them in straight away. The ⋯
|
|
397
397
|
menu names who you are and lets you sign out. Two roles: `admin` manages
|
|
398
398
|
accounts, `member` does everything
|
|
@@ -414,11 +414,30 @@ know before opening it up:
|
|
|
414
414
|
new device, sign in with the password, choose *use a code*, register. A
|
|
415
415
|
password alone never registers a passkey — that is what stops a stolen
|
|
416
416
|
password from enrolling the thief's phone.
|
|
417
|
-
- **Lost
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
password
|
|
421
|
-
|
|
417
|
+
- **Lost something**: another admin opens *recover access…* on your row and
|
|
418
|
+
picks what you lost (always confirming with their own passkey). Nobody can
|
|
419
|
+
recover themselves.
|
|
420
|
+
- *lost password* — one admin; you get a link (48 h), choose a new password,
|
|
421
|
+
then confirm with one of your passkeys, which are all kept;
|
|
422
|
+
- *lost passkey* — a **second admin approves** from their Security panel; you
|
|
423
|
+
then get a device code (48 h) to register a new device;
|
|
424
|
+
- *lost everything* — password and passkeys are wiped **24 hours** later, or
|
|
425
|
+
as soon as a second admin approves; you, the asker or any admin can cancel
|
|
426
|
+
it meanwhile.
|
|
427
|
+
Making or unmaking an admin also needs a second admin's approval. A device
|
|
428
|
+
code for someone who has no passkey yet is still one admin's act. Every
|
|
429
|
+
session opened before a recovery stops working — including one on the lost
|
|
430
|
+
device; removing a passkey or demoting an admin does the same.
|
|
431
|
+
- While it is on, inviting, deleting an account and changing a role also ask
|
|
432
|
+
the admin for their passkey.
|
|
433
|
+
- **Security panel** (🛡 in the header, a badge when something is new): what
|
|
434
|
+
was done to your account — invited, made admin, reset, a passkey added or
|
|
435
|
+
removed, a recovery asked about you — and, for admins, the whole log, the
|
|
436
|
+
requests waiting for their approval, and their own requests with the link or
|
|
437
|
+
code to hand over once done. On anything you don't recognise,
|
|
438
|
+
**This wasn't me** ends your other sessions, removes a passkey you did not
|
|
439
|
+
add, and flags it to every admin. There is no mail yet: this panel is the
|
|
440
|
+
channel, so keep an eye on its badge.
|
|
422
441
|
- While it is on, the instance password (`admin`) **cannot sign in to the
|
|
423
442
|
browser** — it lives in every agent's environment and no one could reset it;
|
|
424
443
|
use a named account. Agents, Telegram and peers keep working (they never use
|
package/dist/accounts.d.ts
CHANGED
|
@@ -13,10 +13,13 @@ export interface Account {
|
|
|
13
13
|
/** Absent until an invitation is redeemed. */
|
|
14
14
|
passwordHash?: string;
|
|
15
15
|
createdAt: number;
|
|
16
|
-
/** Present only while the invitation is outstanding.
|
|
16
|
+
/** Present only while the invitation is outstanding. `reason: "password"`
|
|
17
|
+
* marks a level-1 reset: the passkeys were kept, so redeeming the link sets
|
|
18
|
+
* a password and then asks for one of them. */
|
|
17
19
|
invite?: {
|
|
18
20
|
token: string;
|
|
19
21
|
expiresAt: number;
|
|
22
|
+
reason?: "password";
|
|
20
23
|
};
|
|
21
24
|
/** Registered passkeys — the second factor (see `src/twofactor.ts`). */
|
|
22
25
|
passkeys?: Passkey[];
|
|
@@ -27,6 +30,10 @@ export interface Account {
|
|
|
27
30
|
expiresAt: number;
|
|
28
31
|
by: string;
|
|
29
32
|
};
|
|
33
|
+
/** A session cookie issued before this instant is refused. Set when access
|
|
34
|
+
* is reset, a passkey removed or an admin demoted: without it a session
|
|
35
|
+
* opened before the reset (a stolen one, a lost phone's) outlived it. */
|
|
36
|
+
sessionsAfter?: number;
|
|
30
37
|
}
|
|
31
38
|
export interface Passkey {
|
|
32
39
|
/** Credential id, base64url. */
|
|
@@ -86,10 +93,13 @@ export declare function signSession(user: string, issuedAt: number, secret: Buff
|
|
|
86
93
|
export declare function readSessionFull(token: string, secret: Buffer, now: number, maxAgeMs: number): {
|
|
87
94
|
user: string;
|
|
88
95
|
mfa: boolean;
|
|
96
|
+
issuedAt: number;
|
|
89
97
|
} | null;
|
|
90
98
|
export declare function readSession(token: string, secret: Buffer, now: number, maxAgeMs: number): string | null;
|
|
91
|
-
/**
|
|
92
|
-
|
|
99
|
+
/** Is a session issued at `issuedAt` still good for this account? */
|
|
100
|
+
export declare function sessionAlive(issuedAt: number, acct: Pick<Account, "sessionsAfter">): boolean;
|
|
101
|
+
/** 48 hours: enough to hand the link over by another channel, short enough
|
|
102
|
+
* that a link left lying in a chat stops working. Was a week. */
|
|
93
103
|
export declare const INVITE_TTL_MS: number;
|
|
94
104
|
export declare function newInvite(now: number): {
|
|
95
105
|
token: string;
|
package/dist/accounts.js
CHANGED
|
@@ -124,7 +124,7 @@ export function readSessionFull(token, secret, now, maxAgeMs) {
|
|
|
124
124
|
return null;
|
|
125
125
|
try {
|
|
126
126
|
const user = Buffer.from(u, "base64url").toString("utf8");
|
|
127
|
-
return user ? { user, mfa: mfa === "1" } : null;
|
|
127
|
+
return user ? { user, mfa: mfa === "1", issuedAt } : null;
|
|
128
128
|
}
|
|
129
129
|
catch {
|
|
130
130
|
return null;
|
|
@@ -133,9 +133,13 @@ export function readSessionFull(token, secret, now, maxAgeMs) {
|
|
|
133
133
|
export function readSession(token, secret, now, maxAgeMs) {
|
|
134
134
|
return readSessionFull(token, secret, now, maxAgeMs)?.user ?? null;
|
|
135
135
|
}
|
|
136
|
-
/**
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
/** Is a session issued at `issuedAt` still good for this account? */
|
|
137
|
+
export function sessionAlive(issuedAt, acct) {
|
|
138
|
+
return !acct.sessionsAfter || issuedAt >= acct.sessionsAfter;
|
|
139
|
+
}
|
|
140
|
+
/** 48 hours: enough to hand the link over by another channel, short enough
|
|
141
|
+
* that a link left lying in a chat stops working. Was a week. */
|
|
142
|
+
export const INVITE_TTL_MS = 48 * 3600 * 1000;
|
|
139
143
|
export function newInvite(now) {
|
|
140
144
|
return { token: randomBytes(24).toString("base64url"), expiresAt: now + INVITE_TTL_MS };
|
|
141
145
|
}
|
package/dist/accounts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accounts.js","sourceRoot":"","sources":["../src/accounts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"accounts.js","sourceRoot":"","sources":["../src/accounts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type RequestKind = "passkey-reset" | "full-reset" | "promote" | "demote";
|
|
2
|
+
export interface AdminRequest {
|
|
3
|
+
id: string;
|
|
4
|
+
kind: RequestKind;
|
|
5
|
+
target: string;
|
|
6
|
+
/** The admin who asked. */
|
|
7
|
+
by: string;
|
|
8
|
+
createdAt: number;
|
|
9
|
+
/** The second admin, once approved. */
|
|
10
|
+
approvedBy?: string;
|
|
11
|
+
/** full-reset only: when it takes effect on its own. */
|
|
12
|
+
effectiveAt?: number;
|
|
13
|
+
/** Set when it took effect / was cancelled or refused; the row then only
|
|
14
|
+
* lingers so its asker can read the outcome. */
|
|
15
|
+
doneAt?: number;
|
|
16
|
+
outcome?: "applied" | "cancelled" | "refused" | "expired";
|
|
17
|
+
/** Who cancelled or refused. */
|
|
18
|
+
closedBy?: string;
|
|
19
|
+
/** What the asker hands over once applied: a reset link or a device code. */
|
|
20
|
+
handover?: {
|
|
21
|
+
kind: "link" | "code";
|
|
22
|
+
value: string;
|
|
23
|
+
expiresAt: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** An unapproved request dies after this. */
|
|
27
|
+
export declare const REQUEST_TTL_MS: number;
|
|
28
|
+
/** Level 3's waiting time. */
|
|
29
|
+
export declare const FULL_RESET_DELAY_MS: number;
|
|
30
|
+
/** A closed row stays this long so its outcome (and handover) can be read. */
|
|
31
|
+
export declare const CLOSED_KEEP_MS: number;
|
|
32
|
+
type Verdict = {
|
|
33
|
+
ok: true;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
error: string;
|
|
37
|
+
};
|
|
38
|
+
interface Who {
|
|
39
|
+
name: string;
|
|
40
|
+
role: string;
|
|
41
|
+
}
|
|
42
|
+
interface Acct {
|
|
43
|
+
name: string;
|
|
44
|
+
role: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function newRequest(kind: RequestKind, target: string, by: string, now: number): AdminRequest;
|
|
47
|
+
export declare const isOpen: (r: AdminRequest) => boolean;
|
|
48
|
+
/** May `actor` ask for this? */
|
|
49
|
+
export declare function askVerdict(o: {
|
|
50
|
+
actor: Who | null;
|
|
51
|
+
kind: RequestKind;
|
|
52
|
+
target: string;
|
|
53
|
+
accounts: Acct[];
|
|
54
|
+
}): Verdict;
|
|
55
|
+
/** May `approver` approve this request now? */
|
|
56
|
+
export declare function approveVerdict(r: AdminRequest | undefined, approver: Who | null, now: number): Verdict;
|
|
57
|
+
/**
|
|
58
|
+
* May `who` cancel it? The asker, any OTHER admin (the 24 h of level 3 are
|
|
59
|
+
* worth what the other admins' attention is worth), or the target themself —
|
|
60
|
+
* a request made behind their back is precisely what they must be able to stop.
|
|
61
|
+
*/
|
|
62
|
+
export declare function cancelVerdict(r: AdminRequest | undefined, who: Who | null): Verdict;
|
|
63
|
+
/** Open full-resets whose 24 h are over: to apply now. */
|
|
64
|
+
export declare function dueRequests(list: AdminRequest[], now: number): AdminRequest[];
|
|
65
|
+
/** Close open requests that ran out without approval. */
|
|
66
|
+
export declare function expire(list: AdminRequest[], now: number): AdminRequest[];
|
|
67
|
+
/** Add a request, replacing an open one of the same kind on the same target,
|
|
68
|
+
* and dropping closed rows past their keep window. */
|
|
69
|
+
export declare function upsertRequest(list: AdminRequest[], r: AdminRequest, now: number): AdminRequest[];
|
|
70
|
+
/** What the Security panel shows a viewer: what they may approve, and what they asked. */
|
|
71
|
+
export declare function requestsFor(viewer: Who, list: AdminRequest[], now: number): {
|
|
72
|
+
waiting: AdminRequest[];
|
|
73
|
+
mine: AdminRequest[];
|
|
74
|
+
};
|
|
75
|
+
export declare function describeRequest(r: AdminRequest): string;
|
|
76
|
+
export declare function loadRequests(): AdminRequest[];
|
|
77
|
+
export declare function saveRequests(list: AdminRequest[]): void;
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-admin requests — 2FA v2 (spec docs/superpowers/specs/2026-10-28-2fa-v2-recovery-design.md
|
|
3
|
+
* §§ 4–5). What one admin may no longer do alone while two-factor is on:
|
|
4
|
+
*
|
|
5
|
+
* - `passkey-reset` (level 2): let someone who lost their passkey register a
|
|
6
|
+
* new device — a second admin approves, then a device code is issued;
|
|
7
|
+
* - `full-reset` (level 3): wipe password and passkeys — effective 24 h after
|
|
8
|
+
* the ask, or at once when a second admin approves;
|
|
9
|
+
* - `promote` / `demote`: make or unmake an admin.
|
|
10
|
+
*
|
|
11
|
+
* Stored beside the accounts (`~/.shadok-ai/users/<key>.requests.json`, 600).
|
|
12
|
+
* No timer anywhere: a due request is APPLIED by whoever next reads the list
|
|
13
|
+
* (`dueRequests`), so a restart loses nothing and nothing has to stay alive.
|
|
14
|
+
*/
|
|
15
|
+
import { randomBytes } from "node:crypto";
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { instanceKey } from "./paths.js";
|
|
20
|
+
/** An unapproved request dies after this. */
|
|
21
|
+
export const REQUEST_TTL_MS = 48 * 3600 * 1000;
|
|
22
|
+
/** Level 3's waiting time. */
|
|
23
|
+
export const FULL_RESET_DELAY_MS = 24 * 3600 * 1000;
|
|
24
|
+
/** A closed row stays this long so its outcome (and handover) can be read. */
|
|
25
|
+
export const CLOSED_KEEP_MS = 7 * 24 * 3600 * 1000;
|
|
26
|
+
export function newRequest(kind, target, by, now) {
|
|
27
|
+
return {
|
|
28
|
+
id: randomBytes(12).toString("base64url"),
|
|
29
|
+
kind,
|
|
30
|
+
target,
|
|
31
|
+
by,
|
|
32
|
+
createdAt: now,
|
|
33
|
+
...(kind === "full-reset" ? { effectiveAt: now + FULL_RESET_DELAY_MS } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export const isOpen = (r) => r.doneAt == null;
|
|
37
|
+
/** May `actor` ask for this? */
|
|
38
|
+
export function askVerdict(o) {
|
|
39
|
+
if (!o.actor || o.actor.role !== "admin")
|
|
40
|
+
return { ok: false, error: "only an admin can ask for this" };
|
|
41
|
+
if (o.actor.name === o.target)
|
|
42
|
+
return { ok: false, error: "not for yourself — ask another admin" };
|
|
43
|
+
const t = o.accounts.find((a) => a.name === o.target);
|
|
44
|
+
if (!t)
|
|
45
|
+
return { ok: false, error: `no such account: ${o.target}` };
|
|
46
|
+
if (o.kind === "promote" && t.role === "admin")
|
|
47
|
+
return { ok: false, error: `${o.target} is already an admin` };
|
|
48
|
+
if (o.kind === "demote" && t.role !== "admin")
|
|
49
|
+
return { ok: false, error: `${o.target} is not an admin` };
|
|
50
|
+
return { ok: true };
|
|
51
|
+
}
|
|
52
|
+
/** May `approver` approve this request now? */
|
|
53
|
+
export function approveVerdict(r, approver, now) {
|
|
54
|
+
if (!r || !isOpen(r))
|
|
55
|
+
return { ok: false, error: "no such open request" };
|
|
56
|
+
if (!approver || approver.role !== "admin")
|
|
57
|
+
return { ok: false, error: "only an admin can approve" };
|
|
58
|
+
if (approver.name === r.by)
|
|
59
|
+
return { ok: false, error: "you asked for it — a SECOND admin must approve" };
|
|
60
|
+
if (approver.name === r.target)
|
|
61
|
+
return { ok: false, error: "it concerns you — another admin must approve" };
|
|
62
|
+
if (r.approvedBy)
|
|
63
|
+
return { ok: false, error: "already approved" };
|
|
64
|
+
if (r.kind !== "full-reset" && now - r.createdAt > REQUEST_TTL_MS)
|
|
65
|
+
return { ok: false, error: "this request has expired" };
|
|
66
|
+
return { ok: true };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* May `who` cancel it? The asker, any OTHER admin (the 24 h of level 3 are
|
|
70
|
+
* worth what the other admins' attention is worth), or the target themself —
|
|
71
|
+
* a request made behind their back is precisely what they must be able to stop.
|
|
72
|
+
*/
|
|
73
|
+
export function cancelVerdict(r, who) {
|
|
74
|
+
if (!r || !isOpen(r))
|
|
75
|
+
return { ok: false, error: "no such open request" };
|
|
76
|
+
if (!who)
|
|
77
|
+
return { ok: false, error: "sign in first" };
|
|
78
|
+
if (who.name === r.by || who.name === r.target || who.role === "admin")
|
|
79
|
+
return { ok: true };
|
|
80
|
+
return { ok: false, error: "only an admin, the asker or the person concerned can cancel this" };
|
|
81
|
+
}
|
|
82
|
+
/** Open full-resets whose 24 h are over: to apply now. */
|
|
83
|
+
export function dueRequests(list, now) {
|
|
84
|
+
return list.filter((r) => isOpen(r) && r.kind === "full-reset" && r.effectiveAt != null && now >= r.effectiveAt);
|
|
85
|
+
}
|
|
86
|
+
/** Close open requests that ran out without approval. */
|
|
87
|
+
export function expire(list, now) {
|
|
88
|
+
return list.map((r) => isOpen(r) && r.kind !== "full-reset" && now - r.createdAt > REQUEST_TTL_MS
|
|
89
|
+
? { ...r, doneAt: now, outcome: "expired" }
|
|
90
|
+
: r);
|
|
91
|
+
}
|
|
92
|
+
/** Add a request, replacing an open one of the same kind on the same target,
|
|
93
|
+
* and dropping closed rows past their keep window. */
|
|
94
|
+
export function upsertRequest(list, r, now) {
|
|
95
|
+
return [
|
|
96
|
+
...list.filter((x) => !(isOpen(x) && x.kind === r.kind && x.target === r.target) && !(x.doneAt != null && now - x.doneAt > CLOSED_KEEP_MS)),
|
|
97
|
+
r,
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
/** What the Security panel shows a viewer: what they may approve, and what they asked. */
|
|
101
|
+
export function requestsFor(viewer, list, now) {
|
|
102
|
+
const waiting = viewer.role === "admin"
|
|
103
|
+
? list.filter((r) => approveVerdict(r, viewer, now).ok)
|
|
104
|
+
: [];
|
|
105
|
+
const mine = list.filter((r) => r.by === viewer.name).sort((a, b) => b.createdAt - a.createdAt);
|
|
106
|
+
// Every OTHER admin can see (and cancel) an open full reset they did not ask
|
|
107
|
+
// for, even though approving it is one click away in `waiting` already.
|
|
108
|
+
return { waiting, mine };
|
|
109
|
+
}
|
|
110
|
+
export function describeRequest(r) {
|
|
111
|
+
switch (r.kind) {
|
|
112
|
+
case "passkey-reset": return `${r.by} asks to let ${r.target} register a new device (lost passkey)`;
|
|
113
|
+
case "full-reset": return `${r.by} asks for a FULL reset of ${r.target} (password and passkeys)`;
|
|
114
|
+
case "promote": return `${r.by} asks to make ${r.target} admin`;
|
|
115
|
+
case "demote": return `${r.by} asks to make ${r.target} member`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// ── Storage ──────────────────────────────────────────────────────────────────
|
|
119
|
+
function requestsFile() {
|
|
120
|
+
return path.join(os.homedir(), ".shadok-ai", "users", instanceKey() + ".requests.json");
|
|
121
|
+
}
|
|
122
|
+
export function loadRequests() {
|
|
123
|
+
try {
|
|
124
|
+
const v = JSON.parse(fs.readFileSync(requestsFile(), "utf8"));
|
|
125
|
+
return Array.isArray(v) ? v.filter((r) => r && typeof r.id === "string") : [];
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function saveRequests(list) {
|
|
132
|
+
const f = requestsFile();
|
|
133
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
134
|
+
fs.writeFileSync(f, JSON.stringify(list, null, 2), { mode: 0o600 });
|
|
135
|
+
fs.chmodSync(f, 0o600);
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=admin-requests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-requests.js","sourceRoot":"","sources":["../src/admin-requests.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type SecEventKind = "invited" | "invite-redeemed" | "removed" | "role-changed" | "access-reset" | "passkey-added" | "passkey-removed" | "device-code" | "two-factor" | "request" | "not-me";
|
|
2
|
+
export interface SecEvent {
|
|
3
|
+
id: string;
|
|
4
|
+
at: number;
|
|
5
|
+
kind: SecEventKind;
|
|
6
|
+
/** Who did it (an account name, or "admin" for the instance password). */
|
|
7
|
+
actor: string;
|
|
8
|
+
/** Whose account it concerns. */
|
|
9
|
+
target: string;
|
|
10
|
+
/** Kind-specific detail: the new role, a passkey's label, "on"/"off". */
|
|
11
|
+
detail?: string;
|
|
12
|
+
/** The passkey concerned, so "this wasn't me" can remove it. */
|
|
13
|
+
passkeyId?: string;
|
|
14
|
+
/** For a `not-me`: the event it disowns. */
|
|
15
|
+
about?: string;
|
|
16
|
+
/** For a `request` event: the two-admin request it reports on, so "this
|
|
17
|
+
* wasn't me" can cancel it while it is still open. */
|
|
18
|
+
requestId?: string;
|
|
19
|
+
/** Who has opened it in the panel. */
|
|
20
|
+
seenBy?: string[];
|
|
21
|
+
/** Already disowned by its target — the button is spent. */
|
|
22
|
+
disowned?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Bounds: the log is an audit trail, not an archive. */
|
|
25
|
+
export declare const MAX_EVENTS = 500;
|
|
26
|
+
export declare const MAX_AGE_MS: number;
|
|
27
|
+
export declare function appendEvent(list: SecEvent[], ev: Omit<SecEvent, "id" | "at">, now: number, id?: string): SecEvent[];
|
|
28
|
+
/**
|
|
29
|
+
* Which events concern the viewer as a TARGET, i.e. what they need to hear
|
|
30
|
+
* about: anything someone else did to their account, and every passkey added
|
|
31
|
+
* or removed on it even by "themselves" — whoever holds their password and a
|
|
32
|
+
* code IS "themselves" to the server, which is exactly the case to catch.
|
|
33
|
+
*/
|
|
34
|
+
export declare function aboutYou(e: SecEvent, viewer: string): boolean;
|
|
35
|
+
export interface Inbox {
|
|
36
|
+
/** Events about the viewer, newest first. */
|
|
37
|
+
aboutYou: (SecEvent & {
|
|
38
|
+
unread: boolean;
|
|
39
|
+
})[];
|
|
40
|
+
/** Admins only: every event, newest first. Empty for a member. */
|
|
41
|
+
log: (SecEvent & {
|
|
42
|
+
unread: boolean;
|
|
43
|
+
})[];
|
|
44
|
+
/** What needs the viewer: unread notices about them, and — for an admin —
|
|
45
|
+
* unread "this wasn't me" flags. */
|
|
46
|
+
badge: number;
|
|
47
|
+
}
|
|
48
|
+
export declare function inboxFor(viewer: {
|
|
49
|
+
name: string;
|
|
50
|
+
role: string;
|
|
51
|
+
}, events: SecEvent[]): Inbox;
|
|
52
|
+
/** Mark everything the viewer can see as seen by them. */
|
|
53
|
+
export declare function markSeen(events: SecEvent[], viewer: {
|
|
54
|
+
name: string;
|
|
55
|
+
role: string;
|
|
56
|
+
}): SecEvent[];
|
|
57
|
+
/**
|
|
58
|
+
* What "this wasn't me" undoes for an event, or why it cannot. Every case ends
|
|
59
|
+
* the account's other sessions; a passkey nobody admits to adding is removed.
|
|
60
|
+
* Only the event's TARGET may disown it, once.
|
|
61
|
+
*/
|
|
62
|
+
export declare function notMeEffect(e: SecEvent | undefined, viewer: string): {
|
|
63
|
+
ok: true;
|
|
64
|
+
removePasskey?: string;
|
|
65
|
+
cancelRequest?: string;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
error: string;
|
|
69
|
+
};
|
|
70
|
+
/** One line a person reads, for the panel. */
|
|
71
|
+
export declare function describeEvent(e: SecEvent): string;
|
|
72
|
+
export declare function loadEvents(): SecEvent[];
|
|
73
|
+
export declare function saveEvents(list: SecEvent[]): void;
|
|
74
|
+
/** Record one event. Never throws: losing an audit line must not fail the
|
|
75
|
+
* action it describes (the console line beside each call still exists). */
|
|
76
|
+
export declare function recordEvent(ev: Omit<SecEvent, "id" | "at">): SecEvent | null;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Security panel's events — what happened to accounts and second factors,
|
|
3
|
+
* kept per instance beside the accounts (`~/.shadok-ai/users/<key>.events.json`,
|
|
4
|
+
* 600). Spec: docs/superpowers/specs/2026-10-28-2fa-v2-recovery-design.md § 6.
|
|
5
|
+
*
|
|
6
|
+
* No mail sender exists, so this is THE channel that tells someone what was
|
|
7
|
+
* done to their account, and the place they can say "this wasn't me". Pure
|
|
8
|
+
* cores (`appendEvent`, `inboxFor`, `notMeEffect`) are tested; the file I/O is
|
|
9
|
+
* the thin part.
|
|
10
|
+
*/
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { instanceKey } from "./paths.js";
|
|
16
|
+
/** Bounds: the log is an audit trail, not an archive. */
|
|
17
|
+
export const MAX_EVENTS = 500;
|
|
18
|
+
export const MAX_AGE_MS = 90 * 24 * 3600 * 1000;
|
|
19
|
+
export function appendEvent(list, ev, now, id) {
|
|
20
|
+
const row = { id: id ?? randomBytes(9).toString("base64url"), at: now, ...ev };
|
|
21
|
+
return [...list.filter((e) => now - e.at <= MAX_AGE_MS), row].slice(-MAX_EVENTS);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Which events concern the viewer as a TARGET, i.e. what they need to hear
|
|
25
|
+
* about: anything someone else did to their account, and every passkey added
|
|
26
|
+
* or removed on it even by "themselves" — whoever holds their password and a
|
|
27
|
+
* code IS "themselves" to the server, which is exactly the case to catch.
|
|
28
|
+
*/
|
|
29
|
+
export function aboutYou(e, viewer) {
|
|
30
|
+
if (e.target !== viewer || e.kind === "not-me")
|
|
31
|
+
return false;
|
|
32
|
+
if (e.kind === "passkey-added" || e.kind === "passkey-removed")
|
|
33
|
+
return true;
|
|
34
|
+
return e.actor !== viewer;
|
|
35
|
+
}
|
|
36
|
+
export function inboxFor(viewer, events) {
|
|
37
|
+
const mark = (e) => ({ ...e, unread: !(e.seenBy ?? []).includes(viewer.name) });
|
|
38
|
+
const newest = [...events].sort((a, b) => b.at - a.at);
|
|
39
|
+
const mine = newest.filter((e) => aboutYou(e, viewer.name)).map(mark);
|
|
40
|
+
const admin = viewer.role === "admin";
|
|
41
|
+
const log = admin ? newest.map(mark) : [];
|
|
42
|
+
const flags = admin ? log.filter((e) => e.kind === "not-me" && e.unread).length : 0;
|
|
43
|
+
return { aboutYou: mine, log, badge: mine.filter((e) => e.unread).length + flags };
|
|
44
|
+
}
|
|
45
|
+
/** Mark everything the viewer can see as seen by them. */
|
|
46
|
+
export function markSeen(events, viewer) {
|
|
47
|
+
const visible = (e) => viewer.role === "admin" || aboutYou(e, viewer.name);
|
|
48
|
+
return events.map((e) => visible(e) && !(e.seenBy ?? []).includes(viewer.name) ? { ...e, seenBy: [...(e.seenBy ?? []), viewer.name] } : e);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* What "this wasn't me" undoes for an event, or why it cannot. Every case ends
|
|
52
|
+
* the account's other sessions; a passkey nobody admits to adding is removed.
|
|
53
|
+
* Only the event's TARGET may disown it, once.
|
|
54
|
+
*/
|
|
55
|
+
export function notMeEffect(e, viewer) {
|
|
56
|
+
if (!e || !aboutYou(e, viewer))
|
|
57
|
+
return { ok: false, error: "no such notice for you" };
|
|
58
|
+
if (e.disowned)
|
|
59
|
+
return { ok: false, error: "already reported" };
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
...(e.kind === "passkey-added" && e.passkeyId ? { removePasskey: e.passkeyId } : {}),
|
|
63
|
+
...(e.requestId ? { cancelRequest: e.requestId } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** One line a person reads, for the panel. */
|
|
67
|
+
export function describeEvent(e) {
|
|
68
|
+
const by = e.actor === e.target ? "you" : e.actor;
|
|
69
|
+
switch (e.kind) {
|
|
70
|
+
case "invited": return `${e.actor} invited ${e.target}${e.detail ? " as " + e.detail : ""}`;
|
|
71
|
+
case "invite-redeemed": return `${e.target} set their password from an invitation or reset link`;
|
|
72
|
+
case "removed": return `${e.actor} removed the account ${e.target}`;
|
|
73
|
+
case "role-changed": return `${e.actor} made ${e.target} ${e.detail ?? "?"}`;
|
|
74
|
+
case "access-reset": return `${e.actor} reset ${e.target}'s access (password and passkeys removed)`;
|
|
75
|
+
case "passkey-added": return `a passkey${e.detail ? " “" + e.detail + "”" : ""} was registered on ${e.target}'s account (by ${by})`;
|
|
76
|
+
case "passkey-removed": return `a passkey was removed from ${e.target}'s account (by ${by})`;
|
|
77
|
+
case "device-code": return `${e.actor} issued a device code for ${e.target}`;
|
|
78
|
+
case "two-factor": return `${e.actor} turned two-factor sign-in ${e.detail ?? ""}`.trim();
|
|
79
|
+
case "request": return e.detail ?? `a request about ${e.target}`;
|
|
80
|
+
case "not-me": return `⚠ ${e.actor} reported “this wasn't me”: ${e.detail ?? ""}`.trim();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ── Storage ──────────────────────────────────────────────────────────────────
|
|
84
|
+
function eventsFile() {
|
|
85
|
+
return path.join(os.homedir(), ".shadok-ai", "users", instanceKey() + ".events.json");
|
|
86
|
+
}
|
|
87
|
+
export function loadEvents() {
|
|
88
|
+
try {
|
|
89
|
+
const v = JSON.parse(fs.readFileSync(eventsFile(), "utf8"));
|
|
90
|
+
return Array.isArray(v) ? v.filter((e) => e && typeof e.id === "string") : [];
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function saveEvents(list) {
|
|
97
|
+
const f = eventsFile();
|
|
98
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
99
|
+
fs.writeFileSync(f, JSON.stringify(list, null, 2), { mode: 0o600 });
|
|
100
|
+
fs.chmodSync(f, 0o600);
|
|
101
|
+
}
|
|
102
|
+
/** Record one event. Never throws: losing an audit line must not fail the
|
|
103
|
+
* action it describes (the console line beside each call still exists). */
|
|
104
|
+
export function recordEvent(ev) {
|
|
105
|
+
try {
|
|
106
|
+
const list = appendEvent(loadEvents(), ev, Date.now());
|
|
107
|
+
saveEvents(list);
|
|
108
|
+
return list[list.length - 1];
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
console.log(`security: could not record ${ev.kind} — ${err.message}`);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=security-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-events.js","sourceRoot":"","sources":["../src/security-events.ts"],"names":[],"mappings":"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"}
|