@connextar/house 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +673 -8
- package/dist/admin/access.d.ts +134 -0
- package/dist/admin/access.d.ts.map +1 -0
- package/dist/admin/access.js +157 -0
- package/dist/admin/access.js.map +1 -0
- package/dist/admin/audit.d.ts +130 -0
- package/dist/admin/audit.d.ts.map +1 -0
- package/dist/admin/audit.js +132 -0
- package/dist/admin/audit.js.map +1 -0
- package/dist/admin/bootstrap.d.ts +80 -0
- package/dist/admin/bootstrap.d.ts.map +1 -0
- package/dist/admin/bootstrap.js +106 -0
- package/dist/admin/bootstrap.js.map +1 -0
- package/dist/admin/client-index.d.ts +7 -0
- package/dist/admin/client-index.d.ts.map +1 -0
- package/dist/admin/client-index.js +6 -0
- package/dist/admin/client-index.js.map +1 -0
- package/dist/admin/client.d.ts +72 -0
- package/dist/admin/client.d.ts.map +1 -0
- package/dist/admin/client.js +90 -0
- package/dist/admin/client.js.map +1 -0
- package/dist/admin/index.d.ts +13 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +13 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/proof.d.ts +34 -0
- package/dist/admin/proof.d.ts.map +1 -0
- package/dist/admin/proof.js +30 -0
- package/dist/admin/proof.js.map +1 -0
- package/dist/admin/second-factor-index.d.ts +8 -0
- package/dist/admin/second-factor-index.d.ts.map +1 -0
- package/dist/admin/second-factor-index.js +8 -0
- package/dist/admin/second-factor-index.js.map +1 -0
- package/dist/admin/second-factor.d.ts +156 -0
- package/dist/admin/second-factor.d.ts.map +1 -0
- package/dist/admin/second-factor.js +271 -0
- package/dist/admin/second-factor.js.map +1 -0
- package/dist/admin/testing-index.d.ts +6 -0
- package/dist/admin/testing-index.d.ts.map +1 -0
- package/dist/admin/testing-index.js +6 -0
- package/dist/admin/testing-index.js.map +1 -0
- package/dist/admin/testing.d.ts +84 -0
- package/dist/admin/testing.d.ts.map +1 -0
- package/dist/admin/testing.js +223 -0
- package/dist/admin/testing.js.map +1 -0
- package/dist/admin/view-as.d.ts +209 -0
- package/dist/admin/view-as.d.ts.map +1 -0
- package/dist/admin/view-as.js +251 -0
- package/dist/admin/view-as.js.map +1 -0
- package/dist/guides/guide-figure-zoom.d.ts +33 -8
- package/dist/guides/guide-figure-zoom.d.ts.map +1 -1
- package/dist/guides/guide-figure-zoom.js +34 -9
- package/dist/guides/guide-figure-zoom.js.map +1 -1
- package/dist/guides/library.d.ts +22 -10
- package/dist/guides/library.d.ts.map +1 -1
- package/dist/guides/library.js +3 -0
- package/dist/guides/library.js.map +1 -1
- package/dist/markdown/guide.d.ts +12 -5
- package/dist/markdown/guide.d.ts.map +1 -1
- package/dist/markdown/guide.js +9 -5
- package/dist/markdown/guide.js.map +1 -1
- package/dist/release/changelog.d.ts +19 -0
- package/dist/release/changelog.d.ts.map +1 -1
- package/dist/release/changelog.js +37 -6
- package/dist/release/changelog.js.map +1 -1
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +1 -1
- package/dist/release/index.js.map +1 -1
- package/dist/whats-new/body.d.ts +81 -0
- package/dist/whats-new/body.d.ts.map +1 -0
- package/dist/whats-new/body.js +29 -0
- package/dist/whats-new/body.js.map +1 -0
- package/dist/whats-new/index.d.ts +24 -0
- package/dist/whats-new/index.d.ts.map +1 -0
- package/dist/whats-new/index.js +24 -0
- package/dist/whats-new/index.js.map +1 -0
- package/dist/whats-new/seen.d.ts +23 -0
- package/dist/whats-new/seen.d.ts.map +1 -0
- package/dist/whats-new/seen.js +33 -0
- package/dist/whats-new/seen.js.map +1 -0
- package/dist/whats-new/select.d.ts +89 -0
- package/dist/whats-new/select.d.ts.map +1 -0
- package/dist/whats-new/select.js +117 -0
- package/dist/whats-new/select.js.map +1 -0
- package/dist/whats-new/use-whats-new.d.ts +36 -0
- package/dist/whats-new/use-whats-new.d.ts.map +1 -0
- package/dist/whats-new/use-whats-new.js +81 -0
- package/dist/whats-new/use-whats-new.js.map +1 -0
- package/docs/ReleaseProcess.md +12 -0
- package/package.json +21 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { type AuthWithProofs } from "./proof.js";
|
|
2
|
+
/**
|
|
3
|
+
* Whether this request may use administration — one decision for every app with platform administrators.
|
|
4
|
+
*
|
|
5
|
+
* Six apps wrote it, and they agreed on the rule while disagreeing on its words, its order and what they checked:
|
|
6
|
+
*
|
|
7
|
+
* 1. **An administrator** — read from the database on every request, never from the session payload, which a
|
|
8
|
+
* cookie cache can serve minutes after a demotion (AltEd).
|
|
9
|
+
* 2. **Two-factor is on** for the account.
|
|
10
|
+
* 3. **The session is still live** — read from the session store, always. Better Auth serves a session from its
|
|
11
|
+
* cookie cache for minutes after it was revoked, and the proof below outlives a revoked session; AltEd and Quickie
|
|
12
|
+
* checked this, PharmaLine and PM Tool did not.
|
|
13
|
+
* 4. **This sign-in used the second factor** — the authenticator code, a backup code or a passkey, recorded per
|
|
14
|
+
* session by `adminSecondFactorSessions()` at the moment the session is made.
|
|
15
|
+
* 5. **Not while viewing as somebody.** For as long as a view lasts the administrator is not one: that is what stops
|
|
16
|
+
* the admin panel being reached from inside a view that promised to be read-only (Quickie).
|
|
17
|
+
*
|
|
18
|
+
* Fail-closed throughout: only `true` counts for any fact, so a lookup that answers `undefined` refuses, and a lookup
|
|
19
|
+
* that throws fails the request rather than letting it through.
|
|
20
|
+
*/
|
|
21
|
+
export type AdminRefusalReason = "signed-out" | "not-admin" | "two-factor-off" | "sign-in-without-second-factor" | "session-ended" | "viewing-as";
|
|
22
|
+
export declare const ADMIN_REFUSAL_REASONS: readonly AdminRefusalReason[];
|
|
23
|
+
export interface AdminRefusal {
|
|
24
|
+
ok: false;
|
|
25
|
+
reason: AdminRefusalReason;
|
|
26
|
+
/** The sentence for the page and the 403 alike, so the reason is the same wherever it is met. */
|
|
27
|
+
message: string;
|
|
28
|
+
/** Where to put it right, or null where there is nothing to go to. */
|
|
29
|
+
fixHref: string | null;
|
|
30
|
+
/** 401 when nobody usable is signed in; 403 otherwise. */
|
|
31
|
+
status: 401 | 403;
|
|
32
|
+
}
|
|
33
|
+
export type AdminAccessDecision = {
|
|
34
|
+
ok: true;
|
|
35
|
+
} | AdminRefusal;
|
|
36
|
+
/** What is known about the request. `null` for nobody signed in. */
|
|
37
|
+
export interface AdminAccessFacts {
|
|
38
|
+
isAdmin: boolean;
|
|
39
|
+
twoFactorEnabled: boolean;
|
|
40
|
+
/** The session's sign-in used the second factor (the plugin's record). */
|
|
41
|
+
sessionProven: boolean;
|
|
42
|
+
/** The session still exists in the session store. */
|
|
43
|
+
sessionLive: boolean;
|
|
44
|
+
/** The request is a "view as". */
|
|
45
|
+
viewingAs: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface AdminRefusalWording {
|
|
48
|
+
message?: string;
|
|
49
|
+
fixHref?: string | null;
|
|
50
|
+
}
|
|
51
|
+
/** Per reason, the words and the link an app wants instead of the house's. */
|
|
52
|
+
export type AdminRefusalWords = Partial<Record<AdminRefusalReason, AdminRefusalWording>>;
|
|
53
|
+
export declare const DEFAULT_ADMIN_REFUSAL_WORDS: Record<AdminRefusalReason, Required<AdminRefusalWording>>;
|
|
54
|
+
/** The refusal for a reason, in the app's words where it has them. */
|
|
55
|
+
export declare function adminRefusal(reason: AdminRefusalReason, words?: AdminRefusalWords): AdminRefusal;
|
|
56
|
+
/**
|
|
57
|
+
* The decision, from what is known. Pure, so every refusal is testable without a database.
|
|
58
|
+
*
|
|
59
|
+
* The order is the order the person can act on: who they are, then whether this sign-in still exists, then the
|
|
60
|
+
* account's setup, then this sign-in's, then what the request is doing.
|
|
61
|
+
*/
|
|
62
|
+
export declare function decideAdminAccess(facts: AdminAccessFacts | null | undefined, words?: AdminRefusalWords): AdminRefusal | {
|
|
63
|
+
readonly ok: true;
|
|
64
|
+
};
|
|
65
|
+
type Answer = boolean | null | undefined;
|
|
66
|
+
/** The app's reads. Each is asked only when the answer can still change the decision. */
|
|
67
|
+
export interface AdminAccessLookups {
|
|
68
|
+
/** From the database, every request — never the session payload. */
|
|
69
|
+
isAdmin(userId: string): Answer | Promise<Answer>;
|
|
70
|
+
twoFactorEnabled(userId: string): Answer | Promise<Answer>;
|
|
71
|
+
/** From the session store, every request — the session row exists for this user and has not expired. */
|
|
72
|
+
sessionLive(sessionId: string, userId: string): Answer | Promise<Answer>;
|
|
73
|
+
/** Default: the plugin's record, read through `options.auth`. */
|
|
74
|
+
sessionProven?(sessionId: string, userId: string): Answer | Promise<Answer>;
|
|
75
|
+
}
|
|
76
|
+
export interface AdminAccessOptions {
|
|
77
|
+
/** The Better Auth instance, to read the plugin's record when `sessionProven` is not given. */
|
|
78
|
+
auth?: AuthWithProofs;
|
|
79
|
+
/** The plugin's `recordPrefix`, when it is not the default. */
|
|
80
|
+
recordPrefix?: string;
|
|
81
|
+
words?: AdminRefusalWords;
|
|
82
|
+
}
|
|
83
|
+
/** Who is asking: the app's user with its `sessionId`, `{ userId, sessionId }`, or Better Auth's `getSession` answer. */
|
|
84
|
+
export type SignedIn = {
|
|
85
|
+
id: string;
|
|
86
|
+
sessionId: string;
|
|
87
|
+
} | {
|
|
88
|
+
userId: string;
|
|
89
|
+
sessionId: string;
|
|
90
|
+
} | {
|
|
91
|
+
user: {
|
|
92
|
+
id: string;
|
|
93
|
+
};
|
|
94
|
+
session: {
|
|
95
|
+
id: string;
|
|
96
|
+
};
|
|
97
|
+
} | null | undefined;
|
|
98
|
+
export interface AdminAccess {
|
|
99
|
+
/** The decision for this request. `viewingAs` is whether the request is a "view as". */
|
|
100
|
+
check(signedIn: SignedIn, context?: {
|
|
101
|
+
viewingAs?: boolean;
|
|
102
|
+
}): Promise<AdminAccessDecision>;
|
|
103
|
+
/** The facts the decision was made from, or null for nobody signed in. */
|
|
104
|
+
facts(signedIn: SignedIn, context?: {
|
|
105
|
+
viewingAs?: boolean;
|
|
106
|
+
}): Promise<AdminAccessFacts | null>;
|
|
107
|
+
/** The app's own `isAdmin`, for the view-as rule that an administrator is never a target. */
|
|
108
|
+
isAdmin(userId: string): Promise<boolean>;
|
|
109
|
+
/** The refusal for a reason in this app's words — for a page that reads `?admin=`. */
|
|
110
|
+
refusal(reason: AdminRefusalReason): AdminRefusal;
|
|
111
|
+
readonly words: AdminRefusalWords;
|
|
112
|
+
}
|
|
113
|
+
export declare function adminAccess(lookups: AdminAccessLookups, options?: AdminAccessOptions): AdminAccess;
|
|
114
|
+
/**
|
|
115
|
+
* The refusal named by a page's `?admin=` parameter, for the settings page an administrator is sent to — so it says
|
|
116
|
+
* why in the same words the admin routes refuse with. Anything that is not a reason (`toString`, an array) is null.
|
|
117
|
+
*/
|
|
118
|
+
export declare function adminRefusalFromParam(param: string | string[] | null | undefined, words?: AdminRefusalWords): AdminRefusal | null;
|
|
119
|
+
/**
|
|
120
|
+
* A refusal as an error, for code that answers by throwing. Each app's error handler maps it to its own envelope
|
|
121
|
+
* with `status` and `message`; or pass `makeError` where the house functions take one, to throw the app's own class.
|
|
122
|
+
*/
|
|
123
|
+
export declare class AdminRefusalError extends Error {
|
|
124
|
+
readonly refusal: AdminRefusal;
|
|
125
|
+
readonly status: 401 | 403;
|
|
126
|
+
readonly reason: AdminRefusalReason;
|
|
127
|
+
constructor(refusal: AdminRefusal, options?: {
|
|
128
|
+
cause?: unknown;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** The decision's user, or a thrown refusal: `assertAdminAccess(await access.check(session))`. */
|
|
132
|
+
export declare function assertAdminAccess(decision: AdminAccessDecision, makeError?: (refusal: AdminRefusal) => Error): void;
|
|
133
|
+
export {};
|
|
134
|
+
//# sourceMappingURL=access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/admin/access.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { DEFAULT_ADMIN_PROOF_PREFIX, sessionProofReader } from "./proof.js";
|
|
2
|
+
export const ADMIN_REFUSAL_REASONS = [
|
|
3
|
+
"signed-out",
|
|
4
|
+
"not-admin",
|
|
5
|
+
"two-factor-off",
|
|
6
|
+
"sign-in-without-second-factor",
|
|
7
|
+
"session-ended",
|
|
8
|
+
"viewing-as",
|
|
9
|
+
];
|
|
10
|
+
export const DEFAULT_ADMIN_REFUSAL_WORDS = {
|
|
11
|
+
"signed-out": { message: "Sign in to use administration.", fixHref: "/sign-in" },
|
|
12
|
+
"session-ended": {
|
|
13
|
+
message: "This sign-in has ended. Sign in again to use administration.",
|
|
14
|
+
fixHref: "/sign-in",
|
|
15
|
+
},
|
|
16
|
+
"not-admin": { message: "Only an administrator can do that.", fixHref: null },
|
|
17
|
+
"two-factor-off": {
|
|
18
|
+
message: "Administration needs two-factor authentication on your account. Turn it on in Settings › Account & security, then come back.",
|
|
19
|
+
fixHref: "/settings?admin=two-factor-off#two-factor",
|
|
20
|
+
},
|
|
21
|
+
"sign-in-without-second-factor": {
|
|
22
|
+
message: "Administration needs a sign-in that used your authenticator code or a passkey, and this one didn’t. Sign out, then sign in with your email, password and code — or with a passkey.",
|
|
23
|
+
fixHref: "/settings?admin=sign-in-without-second-factor#two-factor",
|
|
24
|
+
},
|
|
25
|
+
"viewing-as": {
|
|
26
|
+
message: "You’re viewing as somebody else. Stop viewing as them to use administration.",
|
|
27
|
+
fixHref: null,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const STATUS = {
|
|
31
|
+
"signed-out": 401,
|
|
32
|
+
"session-ended": 401,
|
|
33
|
+
"not-admin": 403,
|
|
34
|
+
"two-factor-off": 403,
|
|
35
|
+
"sign-in-without-second-factor": 403,
|
|
36
|
+
"viewing-as": 403,
|
|
37
|
+
};
|
|
38
|
+
/** The refusal for a reason, in the app's words where it has them. */
|
|
39
|
+
export function adminRefusal(reason, words = {}) {
|
|
40
|
+
const own = words[reason];
|
|
41
|
+
const fallback = DEFAULT_ADMIN_REFUSAL_WORDS[reason];
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
reason,
|
|
45
|
+
message: own?.message ?? fallback.message,
|
|
46
|
+
fixHref: own && "fixHref" in own && own.fixHref !== undefined ? own.fixHref : fallback.fixHref,
|
|
47
|
+
status: STATUS[reason],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The decision, from what is known. Pure, so every refusal is testable without a database.
|
|
52
|
+
*
|
|
53
|
+
* The order is the order the person can act on: who they are, then whether this sign-in still exists, then the
|
|
54
|
+
* account's setup, then this sign-in's, then what the request is doing.
|
|
55
|
+
*/
|
|
56
|
+
export function decideAdminAccess(facts, words) {
|
|
57
|
+
const decide = () => {
|
|
58
|
+
if (!facts)
|
|
59
|
+
return "signed-out";
|
|
60
|
+
if (facts.isAdmin !== true)
|
|
61
|
+
return "not-admin";
|
|
62
|
+
if (facts.sessionLive !== true)
|
|
63
|
+
return "session-ended";
|
|
64
|
+
if (facts.twoFactorEnabled !== true)
|
|
65
|
+
return "two-factor-off";
|
|
66
|
+
if (facts.sessionProven !== true)
|
|
67
|
+
return "sign-in-without-second-factor";
|
|
68
|
+
if (facts.viewingAs !== false)
|
|
69
|
+
return "viewing-as";
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
const reason = decide();
|
|
73
|
+
return reason ? adminRefusal(reason, words) : { ok: true };
|
|
74
|
+
}
|
|
75
|
+
function normalise(signedIn) {
|
|
76
|
+
if (!signedIn)
|
|
77
|
+
return null;
|
|
78
|
+
if ("user" in signedIn) {
|
|
79
|
+
const userId = signedIn.user?.id;
|
|
80
|
+
const sessionId = signedIn.session?.id;
|
|
81
|
+
return userId && sessionId ? { userId, sessionId } : null;
|
|
82
|
+
}
|
|
83
|
+
const userId = "userId" in signedIn ? signedIn.userId : signedIn.id;
|
|
84
|
+
return userId && signedIn.sessionId ? { userId, sessionId: signedIn.sessionId } : null;
|
|
85
|
+
}
|
|
86
|
+
export function adminAccess(lookups, options = {}) {
|
|
87
|
+
const words = options.words ?? {};
|
|
88
|
+
const proven = lookups.sessionProven?.bind(lookups) ??
|
|
89
|
+
(options.auth ? sessionProofReader(options.auth, options.recordPrefix ?? DEFAULT_ADMIN_PROOF_PREFIX) : null);
|
|
90
|
+
if (!proven) {
|
|
91
|
+
throw new Error("[admin] adminAccess needs either `sessionProven` or `auth` (the Better Auth instance, to read the record " +
|
|
92
|
+
"adminSecondFactorSessions() writes).");
|
|
93
|
+
}
|
|
94
|
+
const isAdmin = async (userId) => (await lookups.isAdmin(userId)) === true;
|
|
95
|
+
async function facts(signedIn, context = {}) {
|
|
96
|
+
const who = normalise(signedIn);
|
|
97
|
+
if (!who)
|
|
98
|
+
return null;
|
|
99
|
+
const viewingAs = context.viewingAs === true;
|
|
100
|
+
if (!(await isAdmin(who.userId))) {
|
|
101
|
+
return { isAdmin: false, twoFactorEnabled: false, sessionLive: false, sessionProven: false, viewingAs };
|
|
102
|
+
}
|
|
103
|
+
// One round trip: the three reads run together.
|
|
104
|
+
const [twoFactorEnabled, sessionLive, sessionProven] = await Promise.all([
|
|
105
|
+
lookups.twoFactorEnabled(who.userId),
|
|
106
|
+
lookups.sessionLive(who.sessionId, who.userId),
|
|
107
|
+
proven(who.sessionId, who.userId),
|
|
108
|
+
]);
|
|
109
|
+
return {
|
|
110
|
+
isAdmin: true,
|
|
111
|
+
twoFactorEnabled: twoFactorEnabled === true,
|
|
112
|
+
sessionLive: sessionLive === true,
|
|
113
|
+
sessionProven: sessionProven === true,
|
|
114
|
+
viewingAs,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
facts,
|
|
119
|
+
async check(signedIn, context) {
|
|
120
|
+
return decideAdminAccess(await facts(signedIn, context), words);
|
|
121
|
+
},
|
|
122
|
+
isAdmin,
|
|
123
|
+
refusal: (reason) => adminRefusal(reason, words),
|
|
124
|
+
words,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The refusal named by a page's `?admin=` parameter, for the settings page an administrator is sent to — so it says
|
|
129
|
+
* why in the same words the admin routes refuse with. Anything that is not a reason (`toString`, an array) is null.
|
|
130
|
+
*/
|
|
131
|
+
export function adminRefusalFromParam(param, words) {
|
|
132
|
+
if (typeof param !== "string" || !ADMIN_REFUSAL_REASONS.includes(param))
|
|
133
|
+
return null;
|
|
134
|
+
return adminRefusal(param, words);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A refusal as an error, for code that answers by throwing. Each app's error handler maps it to its own envelope
|
|
138
|
+
* with `status` and `message`; or pass `makeError` where the house functions take one, to throw the app's own class.
|
|
139
|
+
*/
|
|
140
|
+
export class AdminRefusalError extends Error {
|
|
141
|
+
refusal;
|
|
142
|
+
status;
|
|
143
|
+
reason;
|
|
144
|
+
constructor(refusal, options) {
|
|
145
|
+
super(refusal.message, options);
|
|
146
|
+
this.name = "AdminRefusalError";
|
|
147
|
+
this.refusal = refusal;
|
|
148
|
+
this.status = refusal.status;
|
|
149
|
+
this.reason = refusal.reason;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** The decision's user, or a thrown refusal: `assertAdminAccess(await access.check(session))`. */
|
|
153
|
+
export function assertAdminAccess(decision, makeError = (refusal) => new AdminRefusalError(refusal)) {
|
|
154
|
+
if (!decision.ok)
|
|
155
|
+
throw makeError(decision);
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/admin/access.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an administrator did, in one shape every app maps to its own `AuditLog`.
|
|
3
|
+
*
|
|
4
|
+
* The apps' trails agree on the columns — actor id, name and role, a dotted action, the entity, a summary under 300
|
|
5
|
+
* characters, a sanitised `changes` — and disagree on scope: AltEd has no tenants, Quickie and PM Tool record
|
|
6
|
+
* platform actions with a null `organizationId`, and nobody recorded an administrator signing in, starting a view,
|
|
7
|
+
* or being granted the role by script. So the event says its scope outright (`platform`, or one tenant), the actor
|
|
8
|
+
* carries their standing at the time, and a "view as" names both people — the administrator stays the actor, so an
|
|
9
|
+
* entry never reads as though the person being viewed did it.
|
|
10
|
+
*
|
|
11
|
+
* The house writes no table. An app passes an `AdminAuditSink` that maps the event to its own row.
|
|
12
|
+
*/
|
|
13
|
+
export interface AdminAuditActor {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
/** The actor's standing when they acted: `"platform-admin"`, `"ADMIN"`, an org role — the app's own word. */
|
|
17
|
+
standing: string;
|
|
18
|
+
}
|
|
19
|
+
export type AdminAuditScope = {
|
|
20
|
+
kind: "platform";
|
|
21
|
+
} | {
|
|
22
|
+
kind: "tenant";
|
|
23
|
+
tenantId: string;
|
|
24
|
+
};
|
|
25
|
+
export interface AdminAuditEvent {
|
|
26
|
+
/** Dotted, e.g. `admin.signed_in`, `user.role_changed`, `admin.view_as_started`. */
|
|
27
|
+
action: string;
|
|
28
|
+
/** Null for a script or the system: the bootstrap script has nobody to name. */
|
|
29
|
+
actor: AdminAuditActor | null;
|
|
30
|
+
/** Set while the actor is viewing as somebody: who. */
|
|
31
|
+
viewingAs?: {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
} | null;
|
|
35
|
+
scope: AdminAuditScope;
|
|
36
|
+
entity: {
|
|
37
|
+
type: string;
|
|
38
|
+
id: string | null;
|
|
39
|
+
};
|
|
40
|
+
/** At most 300 characters — the apps' column. The builders trim it. */
|
|
41
|
+
summary: string;
|
|
42
|
+
changes?: Record<string, unknown>;
|
|
43
|
+
at: Date;
|
|
44
|
+
ip?: string | null;
|
|
45
|
+
userAgent?: string | null;
|
|
46
|
+
}
|
|
47
|
+
export type AdminAuditSink = (event: AdminAuditEvent) => void | Promise<void>;
|
|
48
|
+
export declare const PLATFORM_SCOPE: AdminAuditScope;
|
|
49
|
+
export declare const SUMMARY_MAX = 300;
|
|
50
|
+
/** Where the request came from: the first `x-forwarded-for` hop, and the user agent. */
|
|
51
|
+
export interface RequestMeta {
|
|
52
|
+
ip?: string | null;
|
|
53
|
+
userAgent?: string | null;
|
|
54
|
+
}
|
|
55
|
+
/** `{ ip, userAgent }` from a `Request` or its `Headers`. */
|
|
56
|
+
export declare function requestMeta(source: Request | Headers | null | undefined): RequestMeta;
|
|
57
|
+
interface Common extends RequestMeta {
|
|
58
|
+
at?: Date;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* How a sign-in was made. The first four prove the second factor; the rest do not, and an administrator signed in
|
|
62
|
+
* that way is refused administration until they sign in again.
|
|
63
|
+
*/
|
|
64
|
+
export type AdminSignInMethod = "code" | "backup-code" | "passkey" | "trusted-device" | "password" | "email-link" | "social" | "other";
|
|
65
|
+
export declare function adminSignedIn(input: {
|
|
66
|
+
actor: AdminAuditActor;
|
|
67
|
+
sessionId: string;
|
|
68
|
+
method: AdminSignInMethod;
|
|
69
|
+
/** Whether the session counts for administration — its sign-in used the second factor. */
|
|
70
|
+
proven: boolean;
|
|
71
|
+
scope?: AdminAuditScope;
|
|
72
|
+
} & Common): AdminAuditEvent;
|
|
73
|
+
export declare function roleChanged(input: {
|
|
74
|
+
/** Null for a script. */
|
|
75
|
+
actor: AdminAuditActor | null;
|
|
76
|
+
target: {
|
|
77
|
+
id: string;
|
|
78
|
+
name?: string | null;
|
|
79
|
+
email?: string | null;
|
|
80
|
+
};
|
|
81
|
+
/** The standing before and after: `"user"` → `"platform-admin"`, `"MEMBER"` → `"ADMIN"`. Null for none. */
|
|
82
|
+
from: string | null;
|
|
83
|
+
to: string | null;
|
|
84
|
+
scope?: AdminAuditScope;
|
|
85
|
+
/** What made the change when it was not a screen: `"bootstrap-admin"`. */
|
|
86
|
+
via?: string;
|
|
87
|
+
changes?: Record<string, unknown>;
|
|
88
|
+
} & Common): AdminAuditEvent;
|
|
89
|
+
export declare function viewAsStarted(input: {
|
|
90
|
+
actor: AdminAuditActor;
|
|
91
|
+
target: {
|
|
92
|
+
id: string;
|
|
93
|
+
name: string;
|
|
94
|
+
};
|
|
95
|
+
expiresAt: Date;
|
|
96
|
+
scope?: AdminAuditScope;
|
|
97
|
+
} & Common): AdminAuditEvent;
|
|
98
|
+
export declare function viewAsStopped(input: {
|
|
99
|
+
actor: AdminAuditActor;
|
|
100
|
+
target: {
|
|
101
|
+
id: string;
|
|
102
|
+
name: string;
|
|
103
|
+
};
|
|
104
|
+
scope?: AdminAuditScope;
|
|
105
|
+
} & Common): AdminAuditEvent;
|
|
106
|
+
/** An operator's action across the platform — comps, AI rates, re-pricing — with no tenant of its own. */
|
|
107
|
+
export declare function platformAction(input: {
|
|
108
|
+
actor: AdminAuditActor;
|
|
109
|
+
action: string;
|
|
110
|
+
entity: {
|
|
111
|
+
type: string;
|
|
112
|
+
id?: string | null;
|
|
113
|
+
};
|
|
114
|
+
summary: string;
|
|
115
|
+
changes?: Record<string, unknown>;
|
|
116
|
+
viewingAs?: {
|
|
117
|
+
id: string;
|
|
118
|
+
name: string;
|
|
119
|
+
} | null;
|
|
120
|
+
} & Common): AdminAuditEvent;
|
|
121
|
+
export interface AuditLogger {
|
|
122
|
+
error(message: string, ...args: unknown[]): void;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Recording an event must never break the thing being recorded: an error from the sink is logged and swallowed.
|
|
126
|
+
* Used wherever the house emits an event beside an action that already happened (a sign-in, a view starting).
|
|
127
|
+
*/
|
|
128
|
+
export declare function emitSafely(sink: AdminAuditSink | undefined, auditEvent: AdminAuditEvent, logger?: AuditLogger): Promise<void>;
|
|
129
|
+
export {};
|
|
130
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/admin/audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an administrator did, in one shape every app maps to its own `AuditLog`.
|
|
3
|
+
*
|
|
4
|
+
* The apps' trails agree on the columns — actor id, name and role, a dotted action, the entity, a summary under 300
|
|
5
|
+
* characters, a sanitised `changes` — and disagree on scope: AltEd has no tenants, Quickie and PM Tool record
|
|
6
|
+
* platform actions with a null `organizationId`, and nobody recorded an administrator signing in, starting a view,
|
|
7
|
+
* or being granted the role by script. So the event says its scope outright (`platform`, or one tenant), the actor
|
|
8
|
+
* carries their standing at the time, and a "view as" names both people — the administrator stays the actor, so an
|
|
9
|
+
* entry never reads as though the person being viewed did it.
|
|
10
|
+
*
|
|
11
|
+
* The house writes no table. An app passes an `AdminAuditSink` that maps the event to its own row.
|
|
12
|
+
*/
|
|
13
|
+
export const PLATFORM_SCOPE = { kind: "platform" };
|
|
14
|
+
export const SUMMARY_MAX = 300;
|
|
15
|
+
/** `{ ip, userAgent }` from a `Request` or its `Headers`. */
|
|
16
|
+
export function requestMeta(source) {
|
|
17
|
+
const headers = source instanceof Headers ? source : source?.headers;
|
|
18
|
+
if (!headers)
|
|
19
|
+
return {};
|
|
20
|
+
const forwarded = headers.get("x-forwarded-for")?.split(",")[0]?.trim();
|
|
21
|
+
const ip = forwarded || headers.get("x-real-ip") || null;
|
|
22
|
+
return { ip, userAgent: headers.get("user-agent") };
|
|
23
|
+
}
|
|
24
|
+
const trim = (summary) => (summary.length > SUMMARY_MAX ? `${summary.slice(0, SUMMARY_MAX - 1)}…` : summary);
|
|
25
|
+
function event(base) {
|
|
26
|
+
const { at, ip, userAgent, ...rest } = base;
|
|
27
|
+
return {
|
|
28
|
+
...rest,
|
|
29
|
+
summary: trim(rest.summary),
|
|
30
|
+
at: at ?? new Date(),
|
|
31
|
+
...(ip !== undefined ? { ip } : {}),
|
|
32
|
+
...(userAgent !== undefined ? { userAgent } : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const METHOD_WORDS = {
|
|
36
|
+
code: "their authenticator code",
|
|
37
|
+
"backup-code": "a backup code",
|
|
38
|
+
passkey: "a passkey",
|
|
39
|
+
"trusted-device": "a password on a trusted device",
|
|
40
|
+
password: "a password, without a second factor",
|
|
41
|
+
"email-link": "an email link, without a second factor",
|
|
42
|
+
social: "a social sign-in, without a second factor",
|
|
43
|
+
other: "a sign-in that did not use a second factor",
|
|
44
|
+
};
|
|
45
|
+
export function adminSignedIn(input) {
|
|
46
|
+
return event({
|
|
47
|
+
action: "admin.signed_in",
|
|
48
|
+
actor: input.actor,
|
|
49
|
+
scope: input.scope ?? PLATFORM_SCOPE,
|
|
50
|
+
entity: { type: "session", id: input.sessionId },
|
|
51
|
+
summary: `${input.actor.name} signed in with ${METHOD_WORDS[input.method]}`,
|
|
52
|
+
changes: { method: input.method, proven: input.proven },
|
|
53
|
+
at: input.at,
|
|
54
|
+
ip: input.ip,
|
|
55
|
+
userAgent: input.userAgent,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function roleChanged(input) {
|
|
59
|
+
const who = input.target.name || input.target.email || input.target.id;
|
|
60
|
+
const by = input.via ? ` (${input.via})` : "";
|
|
61
|
+
const summary = input.to === null
|
|
62
|
+
? `Removed ${who}'s ${input.from ?? "role"} standing${by}`
|
|
63
|
+
: `Changed ${who} from ${input.from ?? "no role"} to ${input.to}${by}`;
|
|
64
|
+
return event({
|
|
65
|
+
action: "user.role_changed",
|
|
66
|
+
actor: input.actor,
|
|
67
|
+
scope: input.scope ?? PLATFORM_SCOPE,
|
|
68
|
+
entity: { type: "user", id: input.target.id },
|
|
69
|
+
summary,
|
|
70
|
+
changes: { from: input.from, to: input.to, ...(input.via ? { via: input.via } : {}), ...input.changes },
|
|
71
|
+
at: input.at,
|
|
72
|
+
ip: input.ip,
|
|
73
|
+
userAgent: input.userAgent,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function viewAsStarted(input) {
|
|
77
|
+
return event({
|
|
78
|
+
action: "admin.view_as_started",
|
|
79
|
+
actor: input.actor,
|
|
80
|
+
viewingAs: input.target,
|
|
81
|
+
scope: input.scope ?? PLATFORM_SCOPE,
|
|
82
|
+
entity: { type: "user", id: input.target.id },
|
|
83
|
+
summary: `${input.actor.name} started viewing as ${input.target.name}`,
|
|
84
|
+
changes: { until: input.expiresAt.toISOString() },
|
|
85
|
+
at: input.at,
|
|
86
|
+
ip: input.ip,
|
|
87
|
+
userAgent: input.userAgent,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export function viewAsStopped(input) {
|
|
91
|
+
return event({
|
|
92
|
+
action: "admin.view_as_stopped",
|
|
93
|
+
actor: input.actor,
|
|
94
|
+
viewingAs: input.target,
|
|
95
|
+
scope: input.scope ?? PLATFORM_SCOPE,
|
|
96
|
+
entity: { type: "user", id: input.target.id },
|
|
97
|
+
summary: `${input.actor.name} stopped viewing as ${input.target.name}`,
|
|
98
|
+
at: input.at,
|
|
99
|
+
ip: input.ip,
|
|
100
|
+
userAgent: input.userAgent,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** An operator's action across the platform — comps, AI rates, re-pricing — with no tenant of its own. */
|
|
104
|
+
export function platformAction(input) {
|
|
105
|
+
return event({
|
|
106
|
+
action: input.action,
|
|
107
|
+
actor: input.actor,
|
|
108
|
+
...(input.viewingAs ? { viewingAs: input.viewingAs } : {}),
|
|
109
|
+
scope: PLATFORM_SCOPE,
|
|
110
|
+
entity: { type: input.entity.type, id: input.entity.id ?? null },
|
|
111
|
+
summary: input.summary,
|
|
112
|
+
...(input.changes ? { changes: input.changes } : {}),
|
|
113
|
+
at: input.at,
|
|
114
|
+
ip: input.ip,
|
|
115
|
+
userAgent: input.userAgent,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Recording an event must never break the thing being recorded: an error from the sink is logged and swallowed.
|
|
120
|
+
* Used wherever the house emits an event beside an action that already happened (a sign-in, a view starting).
|
|
121
|
+
*/
|
|
122
|
+
export async function emitSafely(sink, auditEvent, logger = console) {
|
|
123
|
+
if (!sink)
|
|
124
|
+
return;
|
|
125
|
+
try {
|
|
126
|
+
await sink(auditEvent);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
logger.error(`[admin] could not record ${auditEvent.action}`, error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/admin/audit.ts"],"names":[],"mappings":"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"}
|