@gatwech/auth 1.0.0-beta.24 → 1.0.0-beta.25
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/dist/adapters/index.d.mts +1 -1
- package/dist/api/index.d.mts +1 -1
- package/dist/{audit-log-DhRRCMrv.mjs → audit-log-CQnf8nrN.mjs} +442 -46
- package/dist/client/index.d.mts +1 -1
- package/dist/client/plugins/index.d.mts +46 -46
- package/dist/client/plugins/index.mjs +2 -2
- package/dist/client/react/index.d.mts +15 -15
- package/dist/{client-CMsC6-rl.mjs → client-BkpNva-h.mjs} +3 -2
- package/dist/{client-CGV_kj99.d.mts → client-g0OAVuPQ.d.mts} +1 -1
- package/dist/{client-CT-acJeN.d.mts → client-zkWmSxqy.d.mts} +7 -4
- package/dist/cookies/index.d.mts +2 -2
- package/dist/db/index.d.mts +1 -1
- package/dist/{index-Czr_eQFE.d.mts → index-B4HTudhm.d.mts} +6 -6
- package/dist/{index-DYqQWdEv.d.mts → index-BHiF_ZfZ.d.mts} +13 -13
- package/dist/{index-Dmjw6re9.d.mts → index-BX55rZO5.d.mts} +2 -2
- package/dist/{index-Clwezlxm.d.mts → index-BfwCJQfM.d.mts} +8 -8
- package/dist/{index-DGRGLg16.d.mts → index-BhbilPfs.d.mts} +2 -2
- package/dist/{index-JzSy-vFn.d.mts → index-Bi-vEjDm.d.mts} +166 -166
- package/dist/{index-Cl9ITXy1.d.mts → index-BifhxQ8O.d.mts} +10 -10
- package/dist/{index-DWGj7du8.d.mts → index-BiomZttP.d.mts} +19 -19
- package/dist/{index-Hoybqpnt.d.mts → index-BoHrBgTB.d.mts} +9 -9
- package/dist/{index-UDzaYKxq.d.mts → index-C5W_sTlp.d.mts} +9 -9
- package/dist/{index-CLhlur69.d.mts → index-CNyQFga7.d.mts} +6 -6
- package/dist/{index-CZtQ0rUe.d.mts → index-CTjXncwM.d.mts} +113 -113
- package/dist/{index-CGGitykk.d.mts → index-CUUaJp3R.d.mts} +7 -7
- package/dist/{index-D85KXOKz.d.mts → index-CcJjgkwe.d.mts} +3 -3
- package/dist/{index-B-i2ob-C.d.mts → index-Cdfh66xb.d.mts} +9 -9
- package/dist/{index-CHMYUshk.d.mts → index-DBtm_waB.d.mts} +13 -13
- package/dist/{index-DWSbFJWU.d.mts → index-DD_pxX-h.d.mts} +662 -354
- package/dist/{index-BYxt8qOP.d.mts → index-DRoa3BGG.d.mts} +33 -33
- package/dist/{index-D5cyFBEb.d.mts → index-M0IQ_DTL.d.mts} +4 -4
- package/dist/{index-rcUKPBBy.d.mts → index-gRLqvVoc.d.mts} +6 -6
- package/dist/{index-Dt6JhAV4.d.mts → index-i1L-uguJ.d.mts} +6 -6
- package/dist/{index-B6to9CaK.d.mts → index-kBEBsEUr.d.mts} +1 -1
- package/dist/{index-BRUQHEUF.d.mts → index-mDmrBYrk.d.mts} +5 -5
- package/dist/index.d.mts +1 -1
- package/dist/integrations/next-js.d.mts +4 -4
- package/dist/integrations/node.d.mts +1 -1
- package/dist/integrations/react-start.d.mts +2 -2
- package/dist/oauth2/index.d.mts +1 -1
- package/dist/plugins/admin/index.d.mts +2 -2
- package/dist/plugins/anonymous/index.d.mts +2 -2
- package/dist/plugins/audit-log/client.d.mts +23 -23
- package/dist/plugins/audit-log/index.d.mts +23 -23
- package/dist/plugins/audit-log/index.mjs +3 -3
- package/dist/plugins/bearer/index.d.mts +1 -1
- package/dist/plugins/captcha/index.d.mts +1 -1
- package/dist/plugins/custom-session/index.d.mts +2 -2
- package/dist/plugins/device-authorization/index.d.mts +1 -1
- package/dist/plugins/email-otp/index.d.mts +1 -1
- package/dist/plugins/generic-oauth/index.d.mts +2 -2
- package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +23 -23
- package/dist/plugins/index.mjs +5 -5
- package/dist/plugins/jwt/index.d.mts +2 -2
- package/dist/plugins/magic-link/index.d.mts +1 -1
- package/dist/plugins/multi-session/index.d.mts +1 -1
- package/dist/plugins/oauth-proxy/index.d.mts +1 -1
- package/dist/plugins/one-tap/index.d.mts +1 -1
- package/dist/plugins/one-time-token/index.d.mts +2 -2
- package/dist/plugins/one-time-token/index.mjs +4 -4
- package/dist/plugins/organization/index.d.mts +2 -2
- package/dist/plugins/passkey/index.d.mts +2 -2
- package/dist/plugins/phone-number/index.d.mts +2 -2
- package/dist/plugins/sentinel/client.d.mts +23 -23
- package/dist/plugins/sentinel/client.mjs +2 -2
- package/dist/plugins/sentinel/index.d.mts +22 -22
- package/dist/plugins/sentinel/index.mjs +3 -3
- package/dist/plugins/two-factor/index.d.mts +2 -2
- package/dist/plugins/username/index.d.mts +1 -1
- package/dist/{registry-GztLf62p.mjs → registry-BjYhRF79.mjs} +69 -2
- package/dist/{sentinel-b8XEetew.mjs → sentinel-BttZLWPD.mjs} +70 -14
- package/dist/src/plugins/audit-log/error-codes.d.ts +7 -0
- package/dist/src/plugins/audit-log/events.d.ts +13 -0
- package/dist/src/plugins/audit-log/index.d.ts +114 -8
- package/dist/src/plugins/audit-log/integrity.d.ts +20 -0
- package/dist/src/plugins/audit-log/redact.d.ts +18 -0
- package/dist/src/plugins/audit-log/remote.d.ts +1 -0
- package/dist/src/plugins/audit-log/schema.d.ts +25 -0
- package/dist/src/plugins/audit-log/types.d.ts +73 -13
- package/dist/src/plugins/sentinel/actions.d.ts +0 -6
- package/dist/src/plugins/sentinel/client.d.ts +3 -0
- package/dist/src/plugins/sentinel/error-codes.d.ts +6 -0
- package/dist/src/plugins/sentinel/index.d.ts +80 -3
- package/dist/src/plugins/sentinel/types.d.ts +1 -1
- package/dist/test-utils/index.d.mts +2 -2
- package/dist/test-utils/index.mjs +4 -4
- package/dist/types/index.d.mts +1 -1
- package/package.json +3 -3
- /package/dist/{pow-DvkUopsK.mjs → pow-Vna_j4hx.mjs} +0 -0
|
@@ -2,14 +2,131 @@ import { t as getIp } from "./get-request-ip-DSjrEpGo.mjs";
|
|
|
2
2
|
import { t as generateRandomString } from "./random-32E3cU5o.mjs";
|
|
3
3
|
import { n as mergeSchema } from "./date-CMr04reD.mjs";
|
|
4
4
|
import { r as getSessionFromCtx, u as sessionMiddleware } from "./session-BB3oqApE.mjs";
|
|
5
|
-
import { a as
|
|
6
|
-
import { env } from "@gatwech/core/env";
|
|
5
|
+
import { a as CORE_EVENT_TYPES, c as PATH_EVENTS, d as VERIFICATION_PATHS, f as hasRole, i as AUDIT_EVENT_TYPES, l as SECURITY_EVENT_TYPES, n as registerAuditRecorder, o as CREDENTIAL_SIGN_IN_PATHS, r as ALL_EVENT_TYPES, s as EVENT_SEVERITIES, u as SIGN_IN_PATHS } from "./registry-BjYhRF79.mjs";
|
|
6
|
+
import { env, isProduction } from "@gatwech/core/env";
|
|
7
|
+
import { PayvoAuthError } from "@gatwech/core/error";
|
|
7
8
|
import { defineErrorCodes } from "@gatwech/core/utils";
|
|
8
9
|
import { APIError } from "better-call";
|
|
9
10
|
import { createAuthEndpoint, createAuthMiddleware } from "@gatwech/core/api";
|
|
10
11
|
import * as z$1 from "zod";
|
|
12
|
+
import { createHMAC } from "@gatwech/utils/hmac";
|
|
11
13
|
import { betterFetch } from "@better-fetch/fetch";
|
|
12
14
|
|
|
15
|
+
//#region src/plugins/audit-log/error-codes.ts
|
|
16
|
+
const AUDIT_LOG_ERROR_CODES = defineErrorCodes({
|
|
17
|
+
AUDIT_LOG_ACCESS_DENIED: "You don't have access to these audit logs",
|
|
18
|
+
AUDIT_CANNOT_BE_DISABLED: "Core audit events and the local audit store can't be turned off",
|
|
19
|
+
AUDIT_LOG_UNAVAILABLE: "Audit logs are temporarily unavailable",
|
|
20
|
+
AUDIT_RETENTION_TOO_SHORT: "Audit events can't be deleted before the retention minimum has passed",
|
|
21
|
+
AUDIT_SIGNING_SECRET_MISSING: "Audit events must be signed in production: set integrity.secret or PAYVO_AUDIT_SIGNING_SECRET"
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/plugins/audit-log/integrity.ts
|
|
26
|
+
const hmac = createHMAC("SHA-256", "base64url");
|
|
27
|
+
/** The version a signature is made with, and what `secret` is for */
|
|
28
|
+
const CURRENT_VERSION = "v1";
|
|
29
|
+
/**
|
|
30
|
+
* The value as it is stored: the same as `JSON.stringify` makes of it, with
|
|
31
|
+
* sorted keys at every depth, dates as ISO strings and bigints as strings.
|
|
32
|
+
* A property that is `undefined`, a function or a symbol is left out, and
|
|
33
|
+
* such an array element, a number that isn't finite and an invalid date are
|
|
34
|
+
* `null`, so a signed event verifies once the database has stored it.
|
|
35
|
+
*/
|
|
36
|
+
function sorted(value) {
|
|
37
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
38
|
+
switch (typeof value) {
|
|
39
|
+
case "string":
|
|
40
|
+
case "boolean": return value;
|
|
41
|
+
case "number": return Number.isFinite(value) ? value : null;
|
|
42
|
+
case "bigint": return value.toString();
|
|
43
|
+
case "object":
|
|
44
|
+
if (value === null) return null;
|
|
45
|
+
if (Array.isArray(value)) return value.map(sorted);
|
|
46
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => !omittedFromJson(entry)).sort(([first], [second]) => first < second ? -1 : 1).map(([key, entry]) => [key, sorted(entry)]));
|
|
47
|
+
default: return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** A property `JSON.stringify` leaves out */
|
|
51
|
+
const omittedFromJson = (value) => value === void 0 || typeof value === "function" || typeof value === "symbol";
|
|
52
|
+
/**
|
|
53
|
+
* A value as the JSON that is signed: sorted keys at every depth, dates as ISO
|
|
54
|
+
* strings, bigints as strings and no whitespace.
|
|
55
|
+
*
|
|
56
|
+
* It is the encoding payvo-core signs its own audit entries with.
|
|
57
|
+
*/
|
|
58
|
+
function canonicalize(value) {
|
|
59
|
+
return JSON.stringify(sorted(value));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Signs a string with HMAC-SHA256 and returns `v1:<signature>`, where the
|
|
63
|
+
* signature is base64url encoded.
|
|
64
|
+
*/
|
|
65
|
+
async function signAuditPayload(payload, secret) {
|
|
66
|
+
return `${CURRENT_VERSION}:${await hmac.sign(secret, payload)}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The string that is signed for an event: its fields as canonical JSON, with
|
|
70
|
+
* sorted keys at every depth, dates as ISO strings, bigints as strings and
|
|
71
|
+
* no whitespace. `signature` isn't part of it, and a field an event doesn't
|
|
72
|
+
* have is `null`.
|
|
73
|
+
*/
|
|
74
|
+
function canonicalPayload(event) {
|
|
75
|
+
return canonicalize({
|
|
76
|
+
id: event.id,
|
|
77
|
+
eventType: event.eventType,
|
|
78
|
+
status: event.status,
|
|
79
|
+
userId: event.userId ?? null,
|
|
80
|
+
identifier: event.identifier ?? null,
|
|
81
|
+
organizationId: event.organizationId ?? null,
|
|
82
|
+
path: event.path ?? null,
|
|
83
|
+
ipAddress: event.ipAddress ?? null,
|
|
84
|
+
userAgent: event.userAgent ?? null,
|
|
85
|
+
visitorId: event.visitorId ?? null,
|
|
86
|
+
eventData: event.eventData ?? null,
|
|
87
|
+
traceId: event.traceId ?? null,
|
|
88
|
+
requestId: event.requestId ?? null,
|
|
89
|
+
actorType: event.actorType,
|
|
90
|
+
severity: event.severity,
|
|
91
|
+
createdAt: event.createdAt
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Signs an event's fields, as `signAuditPayload` does the string */
|
|
95
|
+
function signAuditEvent(event, secret) {
|
|
96
|
+
return signAuditPayload(canonicalPayload(event), secret);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Whether an event's signature matches its fields, so a row that was changed
|
|
100
|
+
* after it was recorded doesn't verify.
|
|
101
|
+
*
|
|
102
|
+
* `integrity.secret` verifies signatures made with the current key, and
|
|
103
|
+
* `integrity.secrets` maps an older version to its key, so keys can be
|
|
104
|
+
* rotated without signing old rows again. An event without a signature, or
|
|
105
|
+
* with a version that has no key, doesn't verify.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* const events = await auth.api.getAllAuditLogs({ headers });
|
|
110
|
+
* events.events.every((event) => event.verified);
|
|
111
|
+
* await verifyAuditEvent(event, { secret: process.env.PAYVO_AUDIT_SIGNING_SECRET });
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
async function verifyAuditEvent(event, integrity) {
|
|
115
|
+
if (!event.signature) return false;
|
|
116
|
+
const separator = event.signature.indexOf(":");
|
|
117
|
+
if (separator < 1) return false;
|
|
118
|
+
const version = event.signature.slice(0, separator);
|
|
119
|
+
const secret = integrity.secrets?.[version] ?? (version === CURRENT_VERSION ? integrity.secret : void 0);
|
|
120
|
+
if (!secret) return false;
|
|
121
|
+
const { signature: _,...fields } = event;
|
|
122
|
+
try {
|
|
123
|
+
return await hmac.verify(secret, canonicalPayload(fields), event.signature.slice(separator + 1));
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
//#endregion
|
|
13
130
|
//#region src/plugins/audit-log/remote.ts
|
|
14
131
|
/** Sends to the backend: the first attempt and up to 2 retries */
|
|
15
132
|
const SEND_ATTEMPTS = 3;
|
|
@@ -83,6 +200,67 @@ function createRemoteSink(options) {
|
|
|
83
200
|
};
|
|
84
201
|
}
|
|
85
202
|
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/plugins/audit-log/redact.ts
|
|
205
|
+
/**
|
|
206
|
+
* Keys that are always redacted, compared as in `redactAuditData`: in lower
|
|
207
|
+
* case, without `-` or `_`.
|
|
208
|
+
*/
|
|
209
|
+
const ALWAYS_REDACTED_KEYS = new Set([
|
|
210
|
+
"password",
|
|
211
|
+
"newpassword",
|
|
212
|
+
"currentpassword",
|
|
213
|
+
"token",
|
|
214
|
+
"accesstoken",
|
|
215
|
+
"refreshtoken",
|
|
216
|
+
"idtoken",
|
|
217
|
+
"secret",
|
|
218
|
+
"clientsecret",
|
|
219
|
+
"otp",
|
|
220
|
+
"otpcode",
|
|
221
|
+
"verificationcode",
|
|
222
|
+
"backupcodes",
|
|
223
|
+
"authorization",
|
|
224
|
+
"cookie",
|
|
225
|
+
"cardnumber",
|
|
226
|
+
"cvv",
|
|
227
|
+
"pin"
|
|
228
|
+
]);
|
|
229
|
+
const REDACTED = "[REDACTED]";
|
|
230
|
+
/** A key as compared: lower case, without `-` or `_` */
|
|
231
|
+
const normalizeKey = (key) => key.toLowerCase().replace(/[-_]/g, "");
|
|
232
|
+
/**
|
|
233
|
+
* Replaces the value of every sensitive key with `"[REDACTED]"`, at any depth
|
|
234
|
+
* and through arrays. The key stays, so the shape of the data is still
|
|
235
|
+
* visible.
|
|
236
|
+
*
|
|
237
|
+
* A key is sensitive when its name, in lower case and without `-` or `_`,
|
|
238
|
+
* is in the built-in list (passwords, tokens, OTPs, secrets, authorization
|
|
239
|
+
* and cookie headers, card data) or in `sensitiveKeys`. The input isn't
|
|
240
|
+
* changed. Only plain objects and arrays are walked; other values are
|
|
241
|
+
* returned as they are.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* redactAuditData({ user: { id: "1", Access_Token: "abc" } }, [])
|
|
246
|
+
* // { user: { id: "1", Access_Token: "[REDACTED]" } }
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
function redactAuditData(value, sensitiveKeys = []) {
|
|
250
|
+
const sensitive = new Set([...ALWAYS_REDACTED_KEYS, ...sensitiveKeys.map(normalizeKey)]);
|
|
251
|
+
const redact = (current) => {
|
|
252
|
+
if (Array.isArray(current)) return current.map(redact);
|
|
253
|
+
if (!isPlainObject(current)) return current;
|
|
254
|
+
return Object.fromEntries(Object.entries(current).map(([key, entry]) => [key, sensitive.has(normalizeKey(key)) ? REDACTED : redact(entry)]));
|
|
255
|
+
};
|
|
256
|
+
return redact(value);
|
|
257
|
+
}
|
|
258
|
+
function isPlainObject(value) {
|
|
259
|
+
if (typeof value !== "object" || value === null) return false;
|
|
260
|
+
const prototype = Object.getPrototypeOf(value);
|
|
261
|
+
return prototype === Object.prototype || prototype === null;
|
|
262
|
+
}
|
|
263
|
+
|
|
86
264
|
//#endregion
|
|
87
265
|
//#region src/plugins/audit-log/schema.ts
|
|
88
266
|
const schema = { auditLog: { fields: {
|
|
@@ -126,6 +304,26 @@ const schema = { auditLog: { fields: {
|
|
|
126
304
|
type: "json",
|
|
127
305
|
required: false
|
|
128
306
|
},
|
|
307
|
+
traceId: {
|
|
308
|
+
type: "string",
|
|
309
|
+
required: false
|
|
310
|
+
},
|
|
311
|
+
requestId: {
|
|
312
|
+
type: "string",
|
|
313
|
+
required: false
|
|
314
|
+
},
|
|
315
|
+
actorType: {
|
|
316
|
+
type: "string",
|
|
317
|
+
required: false
|
|
318
|
+
},
|
|
319
|
+
severity: {
|
|
320
|
+
type: "string",
|
|
321
|
+
required: false
|
|
322
|
+
},
|
|
323
|
+
signature: {
|
|
324
|
+
type: "string",
|
|
325
|
+
required: false
|
|
326
|
+
},
|
|
129
327
|
createdAt: {
|
|
130
328
|
type: "date",
|
|
131
329
|
required: true
|
|
@@ -134,10 +332,6 @@ const schema = { auditLog: { fields: {
|
|
|
134
332
|
|
|
135
333
|
//#endregion
|
|
136
334
|
//#region src/plugins/audit-log/index.ts
|
|
137
|
-
const AUDIT_LOG_ERROR_CODES = defineErrorCodes({
|
|
138
|
-
AUDIT_LOG_ACCESS_DENIED: "You don't have access to these audit logs",
|
|
139
|
-
AUDIT_LOG_UNAVAILABLE: "Audit logs are temporarily unavailable"
|
|
140
|
-
});
|
|
141
335
|
/** User fields that change as a side effect and aren't profile edits */
|
|
142
336
|
const INTERNAL_USER_FIELDS = new Set([
|
|
143
337
|
"updatedAt",
|
|
@@ -156,11 +350,23 @@ const auditLogQuery = {
|
|
|
156
350
|
offset: z$1.string().meta({ description: "The offset to start from" }).or(z$1.number()).optional(),
|
|
157
351
|
eventType: z$1.string().meta({ description: "Filter by event type. Eg: \"user_signed_in\"" }).optional(),
|
|
158
352
|
identifier: z$1.string().meta({ description: "Filter by the actor's identifier. Eg: \"user@example.com\"" }).optional(),
|
|
353
|
+
traceId: z$1.string().meta({ description: "Filter by the request chain the events belong to" }).optional(),
|
|
159
354
|
organizationId: z$1.string().meta({ description: "Filter by organization" }).optional(),
|
|
160
355
|
userId: z$1.string().meta({ description: "Filter by user" }).optional(),
|
|
161
356
|
startDate: z$1.string().refine(isDate, "Invalid date").meta({ description: "Only return events created at or after this date. Eg: \"2026-01-01\"" }).optional(),
|
|
162
357
|
endDate: z$1.string().refine(isDate, "Invalid date").meta({ description: "Only return events created at or before this date. Eg: \"2026-01-31\"" }).optional()
|
|
163
358
|
};
|
|
359
|
+
/** `00-<trace id>-<parent id>-<flags>` */
|
|
360
|
+
const TRACEPARENT = /^[\da-f]{2}-([\da-f]{32})-[\da-f]{16}-[\da-f]{2}$/i;
|
|
361
|
+
function newId() {
|
|
362
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
363
|
+
}
|
|
364
|
+
/** The catalog's severity, raised to `warning` for a failed or refused event */
|
|
365
|
+
function severityOf(eventType, status) {
|
|
366
|
+
const severity = EVENT_SEVERITIES[eventType];
|
|
367
|
+
if (severity) return severity;
|
|
368
|
+
return status === "success" ? "info" : "warning";
|
|
369
|
+
}
|
|
164
370
|
function toPagination(query) {
|
|
165
371
|
const limit = Number(query.limit) || 50;
|
|
166
372
|
return {
|
|
@@ -184,13 +390,19 @@ const ACCOUNT_ATTEMPT_LIMIT = 50;
|
|
|
184
390
|
const auditLog = (options) => {
|
|
185
391
|
const opts = {
|
|
186
392
|
...options,
|
|
187
|
-
database: options?.database ?? true,
|
|
188
|
-
trackFailedSignIns: options?.trackFailedSignIns ?? true,
|
|
189
393
|
adminRoles: options?.adminRoles ?? ["admin"],
|
|
190
394
|
organizationAdminRoles: options?.organizationAdminRoles ?? ["owner", "admin"],
|
|
191
395
|
showAttemptsOnOwnAccount: options?.showAttemptsOnOwnAccount ?? true
|
|
192
396
|
};
|
|
397
|
+
const minimumRetentionDays = options?.retention?.minimumDays ?? 365;
|
|
398
|
+
if (!Number.isInteger(minimumRetentionDays) || minimumRetentionDays < 1) throw new PayvoAuthError("audit-log: retention.minimumDays must be a whole number of at least 1");
|
|
399
|
+
const integrity = {
|
|
400
|
+
secret: options?.integrity?.secret ?? env.PAYVO_AUDIT_SIGNING_SECRET,
|
|
401
|
+
secrets: options?.integrity?.secrets
|
|
402
|
+
};
|
|
193
403
|
const excluded = new Set(opts.exclude ?? []);
|
|
404
|
+
const excludedCoreEvent = CORE_EVENT_TYPES.find((eventType) => excluded.has(eventType));
|
|
405
|
+
const disabledOption = ["database", "trackFailedSignIns"].find((name) => options?.[name] === false);
|
|
194
406
|
const { sink: remote, misconfigured } = createRemoteSink(opts.remote);
|
|
195
407
|
const mergedSchema = mergeSchema(schema, opts.schema);
|
|
196
408
|
let authContext;
|
|
@@ -205,8 +417,8 @@ const auditLog = (options) => {
|
|
|
205
417
|
/** The user an admin endpoint acted on, when the body doesn't name them */
|
|
206
418
|
const targets = /* @__PURE__ */ new WeakMap();
|
|
207
419
|
/** The organization an organization endpoint acted on */
|
|
208
|
-
async function resolveOrganizationId(auth, path, body,
|
|
209
|
-
const direct = body.organizationId ??
|
|
420
|
+
async function resolveOrganizationId(auth, path, body, returnedBody, activeOrganizationId) {
|
|
421
|
+
const direct = body.organizationId ?? returnedBody.organizationId ?? returnedBody.member?.organizationId ?? returnedBody.invitation?.organizationId ?? (path === "/organization/create" || path === "/organization/update" ? returnedBody.id : void 0);
|
|
210
422
|
if (typeof direct === "string") return direct;
|
|
211
423
|
if (!path.startsWith("/organization/")) return null;
|
|
212
424
|
if (typeof body.teamId === "string") {
|
|
@@ -230,6 +442,29 @@ const auditLog = (options) => {
|
|
|
230
442
|
visitorId: headers?.get("x-visitor-id") ?? null
|
|
231
443
|
};
|
|
232
444
|
}
|
|
445
|
+
/** Each request's trace and request ids, so all its events share them */
|
|
446
|
+
const correlations = /* @__PURE__ */ new WeakMap();
|
|
447
|
+
/**
|
|
448
|
+
* The request's trace id, from `Trace-Id` or a W3C `traceparent`, and its
|
|
449
|
+
* request id, from `Request-Id`. Either is minted when the request has
|
|
450
|
+
* none. Null outside a request.
|
|
451
|
+
*/
|
|
452
|
+
function correlationOf(ctx) {
|
|
453
|
+
const headers = ctx?.headers ?? ctx?.request?.headers;
|
|
454
|
+
if (!headers) return {
|
|
455
|
+
traceId: null,
|
|
456
|
+
requestId: null
|
|
457
|
+
};
|
|
458
|
+
const known = correlations.get(headers);
|
|
459
|
+
if (known) return known;
|
|
460
|
+
const traceparent = TRACEPARENT.exec(headers.get("traceparent") ?? "");
|
|
461
|
+
const correlation = {
|
|
462
|
+
traceId: headers.get("trace-id") || headers.get("x-trace-id") || traceparent?.[1] || newId(),
|
|
463
|
+
requestId: headers.get("request-id") || headers.get("x-request-id") || newId()
|
|
464
|
+
};
|
|
465
|
+
correlations.set(headers, correlation);
|
|
466
|
+
return correlation;
|
|
467
|
+
}
|
|
233
468
|
/**
|
|
234
469
|
* The signed-in user making a request, and the admin impersonating them,
|
|
235
470
|
* when known
|
|
@@ -261,39 +496,78 @@ const auditLog = (options) => {
|
|
|
261
496
|
...actor.id && actor.id !== event.userId ? { actorId: actor.id } : {},
|
|
262
497
|
...actor.impersonatedBy ? { impersonatedBy: actor.impersonatedBy } : {}
|
|
263
498
|
};
|
|
264
|
-
const eventData = Object.keys(attribution).length ? {
|
|
499
|
+
const eventData = redactAuditData(Object.keys(attribution).length ? {
|
|
265
500
|
...event.eventData,
|
|
266
501
|
...attribution
|
|
267
|
-
} : event.eventData ?? null;
|
|
502
|
+
} : event.eventData ?? null, opts.sensitiveKeys);
|
|
503
|
+
const status = event.status ?? "success";
|
|
504
|
+
const inRequest = Boolean(ctx?.headers ?? ctx?.request?.headers);
|
|
505
|
+
const actorType = event.actorType ?? (!inRequest ? "system" : actor.impersonatedBy || attribution.actorId ? "admin" : "user");
|
|
268
506
|
const { id: _,...data } = {
|
|
269
507
|
id: "",
|
|
270
|
-
status: "success",
|
|
271
508
|
userId: null,
|
|
272
509
|
identifier: null,
|
|
273
510
|
organizationId: null,
|
|
274
511
|
...meta,
|
|
512
|
+
...correlationOf(ctx),
|
|
275
513
|
...event,
|
|
514
|
+
status,
|
|
515
|
+
actorType,
|
|
516
|
+
severity: event.severity ?? severityOf(event.eventType, status),
|
|
276
517
|
eventData,
|
|
277
518
|
createdAt: /* @__PURE__ */ new Date()
|
|
278
519
|
};
|
|
279
520
|
let id = auth.generateId({ model: "auditLog" }) || "";
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
521
|
+
const unsigned = {
|
|
522
|
+
...data,
|
|
523
|
+
signature: null
|
|
524
|
+
};
|
|
525
|
+
let signature = integrity.secret && id ? await signAuditEvent({
|
|
526
|
+
...unsigned,
|
|
527
|
+
id
|
|
528
|
+
}, integrity.secret) : null;
|
|
529
|
+
const row = id ? {
|
|
530
|
+
...unsigned,
|
|
531
|
+
signature,
|
|
532
|
+
id
|
|
533
|
+
} : {
|
|
534
|
+
...unsigned,
|
|
535
|
+
signature
|
|
536
|
+
};
|
|
537
|
+
const created = await auth.adapter.create({
|
|
538
|
+
model: "auditLog",
|
|
539
|
+
data: row,
|
|
540
|
+
forceAllowId: Boolean(id)
|
|
541
|
+
}).catch((e) => {
|
|
542
|
+
auth.logger.error("[audit-log] Failed to write audit log", e);
|
|
543
|
+
return null;
|
|
544
|
+
});
|
|
545
|
+
id = created?.id ?? id;
|
|
546
|
+
if (integrity.secret && !signature && created) {
|
|
547
|
+
signature = await signAuditEvent({
|
|
548
|
+
...unsigned,
|
|
283
549
|
id
|
|
284
|
-
}
|
|
285
|
-
|
|
550
|
+
}, integrity.secret);
|
|
551
|
+
await auth.adapter.update({
|
|
286
552
|
model: "auditLog",
|
|
287
|
-
|
|
288
|
-
|
|
553
|
+
where: [{
|
|
554
|
+
field: "id",
|
|
555
|
+
value: id
|
|
556
|
+
}],
|
|
557
|
+
update: { signature }
|
|
289
558
|
}).catch((e) => {
|
|
290
|
-
auth.logger.error("[audit-log] Failed to
|
|
291
|
-
|
|
292
|
-
}))?.id ?? id;
|
|
559
|
+
auth.logger.error("[audit-log] Failed to sign audit log", e);
|
|
560
|
+
});
|
|
293
561
|
}
|
|
562
|
+
id ||= generateRandomString(32);
|
|
563
|
+
if (integrity.secret && !signature) signature = await signAuditEvent({
|
|
564
|
+
...unsigned,
|
|
565
|
+
id
|
|
566
|
+
}, integrity.secret);
|
|
294
567
|
const full = {
|
|
295
|
-
...
|
|
296
|
-
|
|
568
|
+
...unsigned,
|
|
569
|
+
signature,
|
|
570
|
+
id
|
|
297
571
|
};
|
|
298
572
|
remote?.push(full, auth);
|
|
299
573
|
const onEvent = opts.onEvent;
|
|
@@ -336,6 +610,7 @@ const auditLog = (options) => {
|
|
|
336
610
|
for (const field of [
|
|
337
611
|
"eventType",
|
|
338
612
|
"identifier",
|
|
613
|
+
"traceId",
|
|
339
614
|
"userId",
|
|
340
615
|
"organizationId"
|
|
341
616
|
]) {
|
|
@@ -436,7 +711,8 @@ const auditLog = (options) => {
|
|
|
436
711
|
ipAddress: null,
|
|
437
712
|
userAgent: null,
|
|
438
713
|
visitorId: null,
|
|
439
|
-
eventData: null
|
|
714
|
+
eventData: null,
|
|
715
|
+
signature: null
|
|
440
716
|
}));
|
|
441
717
|
}
|
|
442
718
|
/** Organization memberships of a user, empty without the organization plugin */
|
|
@@ -455,7 +731,15 @@ const auditLog = (options) => {
|
|
|
455
731
|
}
|
|
456
732
|
async function runQuery(auth, query) {
|
|
457
733
|
try {
|
|
458
|
-
|
|
734
|
+
const page = await queryEvents(auth, query);
|
|
735
|
+
if (!integrity.secret) return page;
|
|
736
|
+
return {
|
|
737
|
+
...page,
|
|
738
|
+
events: await Promise.all(page.events.map(async (event) => ({
|
|
739
|
+
...event,
|
|
740
|
+
verified: await verifyAuditEvent(event, integrity)
|
|
741
|
+
})))
|
|
742
|
+
};
|
|
459
743
|
} catch (e) {
|
|
460
744
|
auth.logger.error("[audit-log] Failed to query audit logs", e);
|
|
461
745
|
throw new APIError("SERVICE_UNAVAILABLE", { message: AUDIT_LOG_ERROR_CODES.AUDIT_LOG_UNAVAILABLE });
|
|
@@ -464,6 +748,8 @@ const auditLog = (options) => {
|
|
|
464
748
|
return {
|
|
465
749
|
id: "audit-log",
|
|
466
750
|
init(ctx) {
|
|
751
|
+
if (excludedCoreEvent || disabledOption) throw new PayvoAuthError(`${AUDIT_LOG_ERROR_CODES.AUDIT_CANNOT_BE_DISABLED}: ${excludedCoreEvent ?? disabledOption}`);
|
|
752
|
+
if (isProduction && !integrity.secret) throw new PayvoAuthError(AUDIT_LOG_ERROR_CODES.AUDIT_SIGNING_SECRET_MISSING);
|
|
467
753
|
authContext = ctx;
|
|
468
754
|
registerAuditRecorder(ctx, record);
|
|
469
755
|
if (misconfigured) ctx.logger.warn("[audit-log] Missing PAYVO_INFRA_API_URL or PAYVO_INFRA_API_KEY. Remote audit logging is disabled.");
|
|
@@ -564,7 +850,7 @@ const auditLog = (options) => {
|
|
|
564
850
|
return;
|
|
565
851
|
}
|
|
566
852
|
const isCredentialCheck = CREDENTIAL_SIGN_IN_PATHS.includes(ctx.path);
|
|
567
|
-
if (returned instanceof APIError && failed &&
|
|
853
|
+
if (returned instanceof APIError && failed && (isCredentialCheck || VERIFICATION_PATHS.includes(ctx.path))) {
|
|
568
854
|
await record(ctx, {
|
|
569
855
|
eventType: isCredentialCheck ? AUDIT_EVENT_TYPES.USER_SIGN_IN_FAILED : AUDIT_EVENT_TYPES.VERIFICATION_FAILED,
|
|
570
856
|
status: "failure",
|
|
@@ -588,10 +874,10 @@ const auditLog = (options) => {
|
|
|
588
874
|
const eventType = PATH_EVENTS[ctx.path];
|
|
589
875
|
if (!eventType || failed) return;
|
|
590
876
|
if (ctx.path === "/email-otp/send-verification-otp" && body.type !== "email-verification") return;
|
|
591
|
-
const
|
|
877
|
+
const returnedBody = returned ?? {};
|
|
592
878
|
const actor = ctx.headers ? actors.get(ctx.headers) : void 0;
|
|
593
879
|
const target = (ctx.headers && targets.get(ctx.headers)) ?? (ctx.path.startsWith("/admin/") && typeof body.userId === "string" ? body.userId : null);
|
|
594
|
-
const organizationId = await resolveOrganizationId(ctx.context, ctx.path, body,
|
|
880
|
+
const organizationId = await resolveOrganizationId(ctx.context, ctx.path, body, returnedBody, actor?.activeOrganizationId ?? null);
|
|
595
881
|
const eventData = {};
|
|
596
882
|
if (ctx.path === "/unlink-account") eventData.providerId = body.providerId;
|
|
597
883
|
if (ctx.path === "/organization/invite-member") {
|
|
@@ -605,13 +891,13 @@ const auditLog = (options) => {
|
|
|
605
891
|
if (ctx.path.startsWith("/organization/") && ctx.path.endsWith("-role")) eventData.role = body.role ?? body.roleName ?? body.roleId;
|
|
606
892
|
let owner = null;
|
|
607
893
|
if (ctx.path.startsWith("/api-key/")) {
|
|
608
|
-
eventData.keyId = body.keyId ??
|
|
609
|
-
owner =
|
|
894
|
+
eventData.keyId = body.keyId ?? returnedBody.id;
|
|
895
|
+
owner = returnedBody.userId ?? null;
|
|
610
896
|
}
|
|
611
|
-
if (ctx.path.startsWith("/passkey/")) eventData.passkeyId = body.id ??
|
|
897
|
+
if (ctx.path.startsWith("/passkey/")) eventData.passkeyId = body.id ?? returnedBody.id;
|
|
612
898
|
await record(ctx, {
|
|
613
899
|
eventType,
|
|
614
|
-
userId: target ?? actor?.id ??
|
|
900
|
+
userId: target ?? actor?.id ?? returnedBody.user?.id ?? owner,
|
|
615
901
|
identifier: actor?.email ?? identifier,
|
|
616
902
|
organizationId,
|
|
617
903
|
eventData: Object.keys(eventData).length ? eventData : null
|
|
@@ -673,6 +959,38 @@ const auditLog = (options) => {
|
|
|
673
959
|
type: "object",
|
|
674
960
|
nullable: true
|
|
675
961
|
},
|
|
962
|
+
traceId: {
|
|
963
|
+
type: "string",
|
|
964
|
+
nullable: true
|
|
965
|
+
},
|
|
966
|
+
requestId: {
|
|
967
|
+
type: "string",
|
|
968
|
+
nullable: true
|
|
969
|
+
},
|
|
970
|
+
actorType: {
|
|
971
|
+
type: "string",
|
|
972
|
+
enum: [
|
|
973
|
+
"user",
|
|
974
|
+
"admin",
|
|
975
|
+
"system"
|
|
976
|
+
]
|
|
977
|
+
},
|
|
978
|
+
severity: {
|
|
979
|
+
type: "string",
|
|
980
|
+
enum: [
|
|
981
|
+
"info",
|
|
982
|
+
"warning",
|
|
983
|
+
"critical"
|
|
984
|
+
]
|
|
985
|
+
},
|
|
986
|
+
signature: {
|
|
987
|
+
type: "string",
|
|
988
|
+
nullable: true
|
|
989
|
+
},
|
|
990
|
+
verified: {
|
|
991
|
+
type: "boolean",
|
|
992
|
+
description: "Whether the signature matches the event. Only present when signing is configured."
|
|
993
|
+
},
|
|
676
994
|
createdAt: {
|
|
677
995
|
type: "string",
|
|
678
996
|
format: "date-time"
|
|
@@ -724,7 +1042,7 @@ const auditLog = (options) => {
|
|
|
724
1042
|
metadata: { openapi: {
|
|
725
1043
|
operationId: "getAllAuditLogs",
|
|
726
1044
|
summary: "Get all audit logs",
|
|
727
|
-
description: "Get audit events across organizations the user owns or administers, or every event for admins",
|
|
1045
|
+
description: "Get audit events across organizations the user owns or administers, or every event for admins. Each read is recorded as an audit_read event, which is not part of the page it returns.",
|
|
728
1046
|
responses: { 200: {
|
|
729
1047
|
description: "Paginated audit events",
|
|
730
1048
|
content: { "application/json": { schema: {
|
|
@@ -770,6 +1088,38 @@ const auditLog = (options) => {
|
|
|
770
1088
|
type: "object",
|
|
771
1089
|
nullable: true
|
|
772
1090
|
},
|
|
1091
|
+
traceId: {
|
|
1092
|
+
type: "string",
|
|
1093
|
+
nullable: true
|
|
1094
|
+
},
|
|
1095
|
+
requestId: {
|
|
1096
|
+
type: "string",
|
|
1097
|
+
nullable: true
|
|
1098
|
+
},
|
|
1099
|
+
actorType: {
|
|
1100
|
+
type: "string",
|
|
1101
|
+
enum: [
|
|
1102
|
+
"user",
|
|
1103
|
+
"admin",
|
|
1104
|
+
"system"
|
|
1105
|
+
]
|
|
1106
|
+
},
|
|
1107
|
+
severity: {
|
|
1108
|
+
type: "string",
|
|
1109
|
+
enum: [
|
|
1110
|
+
"info",
|
|
1111
|
+
"warning",
|
|
1112
|
+
"critical"
|
|
1113
|
+
]
|
|
1114
|
+
},
|
|
1115
|
+
signature: {
|
|
1116
|
+
type: "string",
|
|
1117
|
+
nullable: true
|
|
1118
|
+
},
|
|
1119
|
+
verified: {
|
|
1120
|
+
type: "boolean",
|
|
1121
|
+
description: "Whether the signature matches the event. Only present when signing is configured."
|
|
1122
|
+
},
|
|
773
1123
|
createdAt: {
|
|
774
1124
|
type: "string",
|
|
775
1125
|
format: "date-time"
|
|
@@ -794,17 +1144,36 @@ const auditLog = (options) => {
|
|
|
794
1144
|
const { user } = ctx.context.session;
|
|
795
1145
|
const { limit: _limit, offset: _offset,...filters } = ctx.query ?? {};
|
|
796
1146
|
const pagination = toPagination(ctx.query ?? {});
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1147
|
+
/** Records the read once the page is built, so it isn't in the page */
|
|
1148
|
+
const recordRead = (page$1, organizationId) => record(ctx, {
|
|
1149
|
+
eventType: AUDIT_EVENT_TYPES.AUDIT_READ,
|
|
1150
|
+
userId: user.id,
|
|
1151
|
+
identifier: user.email,
|
|
1152
|
+
organizationId,
|
|
1153
|
+
actorType: "admin",
|
|
1154
|
+
eventData: {
|
|
1155
|
+
filters,
|
|
1156
|
+
returned: page$1.events.length,
|
|
1157
|
+
total: page$1.total
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
if (hasRole(user.role, opts.adminRoles)) {
|
|
1161
|
+
const page$1 = await runQuery(ctx.context, {
|
|
1162
|
+
...filters,
|
|
1163
|
+
...pagination
|
|
1164
|
+
});
|
|
1165
|
+
await recordRead(page$1, filters.organizationId ?? null);
|
|
1166
|
+
return ctx.json(page$1);
|
|
1167
|
+
}
|
|
801
1168
|
const administered = (await findMemberships(ctx.context, user.id)).filter((m) => hasRole(m.role, opts.organizationAdminRoles)).map((m) => m.organizationId);
|
|
802
1169
|
if (!administered.length || filters.organizationId && !administered.includes(filters.organizationId)) throw new APIError("FORBIDDEN", { message: AUDIT_LOG_ERROR_CODES.AUDIT_LOG_ACCESS_DENIED });
|
|
803
|
-
|
|
1170
|
+
const page = await runQuery(ctx.context, {
|
|
804
1171
|
...filters,
|
|
805
1172
|
...pagination,
|
|
806
1173
|
organizationIds: filters.organizationId ? void 0 : administered
|
|
807
|
-
})
|
|
1174
|
+
});
|
|
1175
|
+
await recordRead(page, filters.organizationId ?? (administered.length === 1 ? administered[0] : null));
|
|
1176
|
+
return ctx.json(page);
|
|
808
1177
|
}),
|
|
809
1178
|
getEventTypes: createAuthEndpoint("/audit-log/get-event-types", {
|
|
810
1179
|
method: "GET",
|
|
@@ -828,21 +1197,48 @@ const auditLog = (options) => {
|
|
|
828
1197
|
}, async (ctx) => ctx.json({ types: ALL_EVENT_TYPES })),
|
|
829
1198
|
deleteOldEvents: createAuthEndpoint("/audit-log/delete-old-events", {
|
|
830
1199
|
method: "POST",
|
|
831
|
-
body: z$1.object({ olderThanDays: z$1.number().int().min(1).meta({ description: "Delete events recorded more than this many days ago. There is no default
|
|
1200
|
+
body: z$1.object({ olderThanDays: z$1.number().int().min(1).meta({ description: "Delete events recorded more than this many days ago. There is no default, and a period shorter than retention.minimumDays is refused." }) }),
|
|
832
1201
|
metadata: {
|
|
833
1202
|
SERVER_ONLY: true,
|
|
834
|
-
client: false
|
|
1203
|
+
client: false,
|
|
1204
|
+
openapi: {
|
|
1205
|
+
operationId: "deleteOldAuditEvents",
|
|
1206
|
+
summary: "Delete old audit events",
|
|
1207
|
+
description: "Delete audit events recorded more than olderThanDays ago. A period shorter than retention.minimumDays is refused, and every deletion is recorded as an audit_events_deleted event.",
|
|
1208
|
+
responses: { 200: {
|
|
1209
|
+
description: "How many events were deleted",
|
|
1210
|
+
content: { "application/json": { schema: {
|
|
1211
|
+
type: "object",
|
|
1212
|
+
properties: { deleted: { type: "number" } },
|
|
1213
|
+
required: ["deleted"]
|
|
1214
|
+
} } }
|
|
1215
|
+
} }
|
|
1216
|
+
}
|
|
835
1217
|
}
|
|
836
1218
|
}, async (ctx) => {
|
|
837
|
-
|
|
1219
|
+
const { olderThanDays } = ctx.body;
|
|
1220
|
+
if (olderThanDays < minimumRetentionDays) throw new APIError("BAD_REQUEST", { message: AUDIT_LOG_ERROR_CODES.AUDIT_RETENTION_TOO_SHORT });
|
|
1221
|
+
const before = /* @__PURE__ */ new Date(Date.now() - olderThanDays * 864e5);
|
|
838
1222
|
const deleted = await ctx.context.adapter.deleteMany({
|
|
839
1223
|
model: "auditLog",
|
|
840
1224
|
where: [{
|
|
841
1225
|
field: "createdAt",
|
|
842
1226
|
operator: "lt",
|
|
843
|
-
value:
|
|
1227
|
+
value: before
|
|
844
1228
|
}]
|
|
845
1229
|
});
|
|
1230
|
+
const session = await getSessionFromCtx(ctx).catch(() => null);
|
|
1231
|
+
await record(ctx, {
|
|
1232
|
+
eventType: AUDIT_EVENT_TYPES.AUDIT_EVENTS_DELETED,
|
|
1233
|
+
userId: session?.user.id ?? null,
|
|
1234
|
+
identifier: session?.user.email ?? null,
|
|
1235
|
+
actorType: session ? "admin" : "system",
|
|
1236
|
+
eventData: {
|
|
1237
|
+
olderThanDays,
|
|
1238
|
+
before: before.toISOString(),
|
|
1239
|
+
deleted
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
846
1242
|
return ctx.json({ deleted });
|
|
847
1243
|
})
|
|
848
1244
|
},
|
|
@@ -853,4 +1249,4 @@ const auditLog = (options) => {
|
|
|
853
1249
|
};
|
|
854
1250
|
|
|
855
1251
|
//#endregion
|
|
856
|
-
export {
|
|
1252
|
+
export { verifyAuditEvent as n, AUDIT_LOG_ERROR_CODES as r, auditLog as t };
|