@payfit/audit-log 3.17.11 → 3.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +196 -97
- package/dist/metrics.d.ts +1 -1
- package/dist/validation.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@ The SDK is an npm package that sends audit-log records to the durable SQS ingest
|
|
|
4
4
|
|
|
5
5
|
## Using the SDK in your project
|
|
6
6
|
|
|
7
|
-
First, install it
|
|
7
|
+
First, install it:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm i @payfit/audit-log
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Then,
|
|
13
|
+
Then, instantiate a client:
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
import { AuditLogger } from '@payfit/audit-log'
|
|
@@ -43,6 +43,29 @@ logger.log({
|
|
|
43
43
|
})
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
## Validation and failure handling
|
|
47
|
+
|
|
48
|
+
Before sending an audit log to SQS, the SDK validates values that would either
|
|
49
|
+
fail PostgreSQL persistence or be changed silently by `JSON.stringify`.
|
|
50
|
+
|
|
51
|
+
Validation rejects:
|
|
52
|
+
|
|
53
|
+
- U+0000 and unpaired UTF-16 surrogates in persisted strings
|
|
54
|
+
- Empty required fields and strings exceeding their PostgreSQL column limit
|
|
55
|
+
- Circular references and `BigInt` values
|
|
56
|
+
- `undefined`, functions, and symbols in `payload` or `scope`
|
|
57
|
+
- `NaN`, `Infinity`, and `-Infinity` in `payload` or `scope`
|
|
58
|
+
|
|
59
|
+
`userIp` is forwarded unchanged so non-standard but recoverable values are not
|
|
60
|
+
discarded.
|
|
61
|
+
|
|
62
|
+
`AuditLogger.log()` remains non-throwing. On validation failure, it does not send
|
|
63
|
+
the record to SQS, increments the `validation_failed` metric, and emits an
|
|
64
|
+
`audit-log validation failed` error containing `validation.path` and
|
|
65
|
+
`validation.reason`. The error also contains the complete audit record so it can
|
|
66
|
+
be recovered; configure log access and retention accordingly because the record
|
|
67
|
+
may contain personal data.
|
|
68
|
+
|
|
46
69
|
## Contributing
|
|
47
70
|
|
|
48
71
|
### Building the SDK
|
|
@@ -59,9 +82,13 @@ from the repository root (where the workspace `package.json` is located).
|
|
|
59
82
|
|
|
60
83
|
### Testing
|
|
61
84
|
|
|
62
|
-
|
|
85
|
+
Run the SDK test suite from the repository root:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
corepack yarn nx run sdk:test
|
|
89
|
+
```
|
|
63
90
|
|
|
64
|
-
### Playing around with
|
|
91
|
+
### Playing around with the SDK
|
|
65
92
|
|
|
66
93
|
Could it be for testing some changes you made, or just to discover how to use the SDK, there is a [playground](./playground) at your disposal.
|
|
67
94
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,11 +4,11 @@ import { IAuditLogTransport } from './transport/interface';
|
|
|
4
4
|
/**
|
|
5
5
|
* The allowed Namespaces for the AuditLog
|
|
6
6
|
*/
|
|
7
|
-
export type AuditNamespaces = 'employee' | 'company' | 'permissions' | 'absence' | 'expense' | 'payments' | 'auth' | 'billing' | 'modules' | 'integrations' | 'partnerApi' | 'donation' | 'notificationSettings' | 'people' | 'subscriptions' | 'organization' | 'declarations' | 'pricing' | 'payroll' | 'jetlang' | 'files' | 'versioner' | 'live-timing';
|
|
7
|
+
export type AuditNamespaces = 'employee' | 'company' | 'permissions' | 'absence' | 'expense' | 'payments' | 'auth' | 'billing' | 'modules' | 'integrations' | 'partnerApi' | 'donation' | 'notificationSettings' | 'people' | 'subscriptions' | 'organization' | 'declarations' | 'pricing' | 'payroll' | 'jetlang' | 'files' | 'versioner' | 'live-timing' | 'employee-insurance';
|
|
8
8
|
/**
|
|
9
9
|
* The allowed targets for the AuditLog
|
|
10
10
|
*/
|
|
11
|
-
export type AuditTargets = 'reporting' | 'employee' | 'employee.settings' | 'employee-request' | 'company' | 'company.billings' | 'company.billing' | 'company.variables' | 'company.settings' | 'company.emails' | 'company.addons' | 'company.plans' | 'company.modules' | 'declaration.metadata' | 'user.role' | 'user.account' | 'user.primaryEmail' | 'user.secondaryEmail' | 'user.locale' | 'user.notificationSettings' | 'user.mfa' | 'role' | 'team' | 'department' | 'workflow' | 'integration' | 'apiAccess' | 'individual' | 'association' | 'association.archived' | 'donation' | 'donation.amount' | 'donation.auth.partner.portal' | 'donation.company.onboarding' | 'donation.taxReceipt' | 'donationPayment.status' | 'people.document.comments' | 'people.document.notifications' | 'people.document.reactions' | 'people.document.status' | 'people.document.answers' | 'people.oneOne' | 'people.campaign' | 'people.assessment' | 'people.assessment.360details' | 'people.assessment.managerRequests' | 'people.assessment.manager' | 'people.assessment.answers' | 'people.assessment.feedbacks' | 'iban' | 'salary-payments.connect' | 'salary-payments.pay.url' | 'advance-salary-payments.pay.url' | 'variable' | 'jetlang-data' | 'access-logs' | 'event' | 'topic' | 'file' | 'file.presigned-url' | 'archive' | 'payslip.merge';
|
|
11
|
+
export type AuditTargets = 'reporting' | 'employee' | 'employee.settings' | 'employee-request' | 'company' | 'company.billings' | 'company.billing' | 'company.variables' | 'company.settings' | 'company.emails' | 'company.addons' | 'company.plans' | 'company.modules' | 'declaration.metadata' | 'user.role' | 'user.account' | 'user.primaryEmail' | 'user.secondaryEmail' | 'user.locale' | 'user.notificationSettings' | 'user.mfa' | 'role' | 'team' | 'department' | 'workflow' | 'integration' | 'apiAccess' | 'individual' | 'association' | 'association.archived' | 'donation' | 'donation.amount' | 'donation.auth.partner.portal' | 'donation.company.onboarding' | 'donation.taxReceipt' | 'donationPayment.status' | 'people.document.comments' | 'people.document.notifications' | 'people.document.reactions' | 'people.document.status' | 'people.document.answers' | 'people.oneOne' | 'people.campaign' | 'people.assessment' | 'people.assessment.360details' | 'people.assessment.managerRequests' | 'people.assessment.manager' | 'people.assessment.answers' | 'people.assessment.feedbacks' | 'iban' | 'salary-payments.connect' | 'salary-payments.pay.url' | 'advance-salary-payments.pay.url' | 'variable' | 'jetlang-data' | 'access-logs' | 'event' | 'topic' | 'file' | 'file.presigned-url' | 'archive' | 'payslip.merge' | 'employee-insurance.employer' | 'employee-insurance.token.employer' | 'employee-insurance.token.employee';
|
|
12
12
|
/**
|
|
13
13
|
* The allowed Events for the AuditLog
|
|
14
14
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var E=Object.defineProperty;var b=(t,e,r)=>e in t?E(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var n=(t,e,r)=>b(t,typeof e!="symbol"?e+"":e,r);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const _=require("uuid"),d=require("winston"),A=require("hot-shots"),y=require("@aws-sdk/client-sqs"),L=require("zlib"),q="audit-logs",v="@payfit/audit-log",I=()=>{const t=process.env.LOG_LEVEL;return t&&t.trim()!==""?t.trim():"info"},U=(t,e)=>d.createLogger({level:I(),silent:process.env.NODE_ENV==="test",exitOnError:!1,defaultMeta:{namespace:q,sdk:v,service:t,environment:e},format:d.format.combine(d.format.timestamp(),d.format.errors({stack:!0}),d.format.json()),transports:[new d.transports.Console]}),M="payfit.audit_logs.",S=t=>Object.entries(t).filter(([,e])=>e!==void 0&&e!=="").map(([e,r])=>`${e}:${String(r)}`);class ${constructor(){n(this,"client");this.client=new A.StatsD({prefix:M,errorHandler:()=>{},cacheDns:!0})}increment(e,r){this.client.increment(e,1,S(r))}histogram(e,r,i){this.client.histogram(e,r,S(i))}async close(){await new Promise(e=>{this.client.close(()=>e())})}}const z=()=>new $;function C(t){return L.brotliCompressSync(new TextEncoder().encode(t)).toString("base64")}function O(t){return t instanceof Error&&t.message.includes("Message must be shorter than")}const D="eu-west-3",N={dev:"https://sqs.eu-west-3.amazonaws.com/533547282673/audit-logs-ingest",staging:"https://sqs.eu-west-3.amazonaws.com/589883667243/audit-logs-ingest",prod:"https://sqs.eu-west-3.amazonaws.com/614948436281/audit-logs-ingest"},x={working:"staging",production:"prod",development:"dev"};class R{constructor(e,r){n(this,"environment");n(this,"logger");n(this,"client");n(this,"queueUrl");this.environment=e,this.logger=r;const i=x[e]??e;if(this.queueUrl=N[i],!this.queueUrl){r==null||r.warn("audit-logs SDK: no SQS queue URL configured for this environment, audit logs are disabled",{environment:e});return}this.client=new y.SQSClient({region:D})}async create(e){var i;if(!this.client||!this.queueUrl)return{compressed:!1};const r=JSON.stringify(e);try{return await this.send(r),{compressed:!1}}catch(c){if(!O(c))throw c;return(i=this.logger)==null||i.info("audit-log payload exceeds the SQS size limit, retrying with Brotli compression",{id:e.id,namespace:e.namespace}),await this.send(C(r)),{compressed:!0}}}async send(e){await this.client.send(new y.SendMessageCommand({QueueUrl:this.queueUrl,MessageBody:e}))}}class o extends Error{constructor(r,i){super(`${r}: ${i}`);n(this,"path");n(this,"reason");this.path=r,this.reason=i,this.name="AuditLogValidationError"}}const T={countryCode:2,internalId:64,namespace:64,service:64,event:64,target:64,environment:64,holdingId:24,companyId:24,employeeId:24,individualId:24,roleId:24,specialRole:32,email:320},P=new Set(["namespace","service","event","target","environment"]),Q=t=>{for(let e=0;e<t.length;e+=1){const r=t.charCodeAt(e);if(r>=55296&&r<=56319){if(e+1>=t.length)return!0;const i=t.charCodeAt(e+1);if(i<56320||i>57343)return!0;e+=1}else if(r>=56320&&r<=57343)return!0}return!1},u=(t,e,r={})=>{if(t==null){if(r.required)throw new o(e,"must not be empty");return}if(typeof t!="string")throw new o(e,"must be a string");if(r.required&&t==="")throw new o(e,"must not be empty");if(t.includes("\0"))throw new o(e,"contains U+0000");if(Q(t))throw new o(e,"contains unpaired UTF-16 surrogate");if(r.limit!==void 0&&Array.from(t).length>r.limit)throw new o(e,`exceeds PostgreSQL limit of ${r.limit} characters`)},j=(t,e)=>/^[A-Za-z_][A-Za-z0-9_]*$/.test(e)?`${t}.${e}`:`${t}[key]`,m=(t,e,r)=>{if(typeof t=="string"){u(t,e);return}if(typeof t=="bigint")throw new o(e,"BigInt is not JSON-serializable");if(typeof t>"u"||typeof t=="function"||typeof t=="symbol")throw new o(e,`${typeof t} is not JSON-serializable`);if(typeof t=="number"&&!Number.isFinite(t))throw new o(e,"must be a finite number");if(!(t===null||typeof t!="object")){if(r.has(t))throw new o(e,"contains a circular reference");r.add(t),Array.isArray(t)?t.forEach((i,c)=>m(i,`${e}[${c}]`,r)):Object.entries(t).forEach(([i,c])=>{u(i,e),m(c,j(e,i),r)}),r.delete(t)}},F=t=>{for(const[e,r]of Object.entries(T))u(t[e],e,{limit:r,required:P.has(e)});t.payload!==void 0&&t.payload!==null&&m(t.payload,"payload",new Set),t.scope!==void 0&&t.scope!==null&&m(t.scope,"scope",new Set)};class V{constructor(e,r={}){n(this,"params");n(this,"transport");n(this,"logger");n(this,"metrics");this.params=e,this.logger=r.logger??U(e.service,e.environment),this.transport=r.transport??new R(e.environment,this.logger),this.metrics=r.metrics??z()}async log({scope:e,account:r,...i}){const c=_.v4(),{environment:f,service:h}=this.params,s={...i,...r,service:h,id:c,environment:f,...e&&{scope:e}},l={service:h,environment:f,namespace:s.namespace,event:s.event,target:s.target},w=Date.now();try{F(s);const{compressed:g}=await this.transport.create(s);this.metrics.increment("sent",{...l,compressed:String(g)}),this.metrics.histogram("send_duration_ms",Date.now()-w,l),this.logger.debug("audit-log sent",{id:s.id,event:s.event,target:s.target})}catch(g){const a=g instanceof o?g:void 0;this.metrics.increment(a?"validation_failed":"send_failed",l),this.metrics.histogram("send_duration_ms",Date.now()-w,l);const p=g.message;try{this.logger.error(a?"audit-log validation failed":"audit-log write failed",{audit_log:s,error:p,...a&&{validation:{path:a.path,reason:a.reason}}})}catch{this.metrics.increment("serialization_failed",l);try{this.logger.error(a?"audit-log validation failed (serialization failed)":"audit-log write failed (serialization failed)",{audit_log_meta:{id:s.id,service:s.service,environment:s.environment,namespace:s.namespace,event:s.event,target:s.target},error:p,...a&&{validation:{path:a.path,reason:a.reason}}})}catch{}}}}}exports.AuditLogger=V;
|
package/dist/index.mjs
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
var
|
|
2
|
-
var
|
|
3
|
-
var
|
|
4
|
-
import { v4 as
|
|
5
|
-
import { createLogger as
|
|
6
|
-
import { StatsD as
|
|
7
|
-
import { SQSClient as
|
|
8
|
-
import { brotliCompressSync as
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
return
|
|
12
|
-
},
|
|
13
|
-
level:
|
|
1
|
+
var S = Object.defineProperty;
|
|
2
|
+
var E = (t, e, r) => e in t ? S(t, e, { enumerable: !0, configurable: !0, writable: !0, value: r }) : t[e] = r;
|
|
3
|
+
var n = (t, e, r) => E(t, typeof e != "symbol" ? e + "" : e, r);
|
|
4
|
+
import { v4 as _ } from "uuid";
|
|
5
|
+
import { createLogger as b, transports as A, format as m } from "winston";
|
|
6
|
+
import { StatsD as L } from "hot-shots";
|
|
7
|
+
import { SQSClient as v, SendMessageCommand as I } from "@aws-sdk/client-sqs";
|
|
8
|
+
import { brotliCompressSync as U } from "zlib";
|
|
9
|
+
const q = "audit-logs", C = "@payfit/audit-log", M = () => {
|
|
10
|
+
const t = process.env.LOG_LEVEL;
|
|
11
|
+
return t && t.trim() !== "" ? t.trim() : "info";
|
|
12
|
+
}, $ = (t, e) => b({
|
|
13
|
+
level: M(),
|
|
14
14
|
silent: process.env.NODE_ENV === "test",
|
|
15
15
|
exitOnError: !1,
|
|
16
16
|
defaultMeta: {
|
|
17
|
-
namespace:
|
|
18
|
-
sdk:
|
|
19
|
-
service:
|
|
17
|
+
namespace: q,
|
|
18
|
+
sdk: C,
|
|
19
|
+
service: t,
|
|
20
20
|
environment: e
|
|
21
21
|
},
|
|
22
|
-
format:
|
|
23
|
-
transports: [new
|
|
24
|
-
}),
|
|
25
|
-
class
|
|
22
|
+
format: m.combine(m.timestamp(), m.errors({ stack: !0 }), m.json()),
|
|
23
|
+
transports: [new A.Console()]
|
|
24
|
+
}), z = "payfit.audit_logs.", y = (t) => Object.entries(t).filter(([, e]) => e !== void 0 && e !== "").map(([e, r]) => `${e}:${String(r)}`);
|
|
25
|
+
class O {
|
|
26
26
|
constructor() {
|
|
27
|
-
|
|
28
|
-
this.client = new
|
|
29
|
-
prefix:
|
|
27
|
+
n(this, "client");
|
|
28
|
+
this.client = new L({
|
|
29
|
+
prefix: z,
|
|
30
30
|
// Loss of metrics never breaks the caller — DogStatsD UDP is best-effort by design.
|
|
31
31
|
errorHandler: () => {
|
|
32
32
|
},
|
|
33
33
|
cacheDns: !0
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
-
increment(e,
|
|
37
|
-
this.client.increment(e, 1,
|
|
36
|
+
increment(e, r) {
|
|
37
|
+
this.client.increment(e, 1, y(r));
|
|
38
38
|
}
|
|
39
|
-
histogram(e,
|
|
40
|
-
this.client.histogram(e,
|
|
39
|
+
histogram(e, r, i) {
|
|
40
|
+
this.client.histogram(e, r, y(i));
|
|
41
41
|
}
|
|
42
42
|
async close() {
|
|
43
43
|
await new Promise((e) => {
|
|
@@ -45,110 +45,209 @@ class b {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
const D = () => new
|
|
49
|
-
function N(
|
|
50
|
-
return
|
|
48
|
+
const D = () => new O();
|
|
49
|
+
function N(t) {
|
|
50
|
+
return U(new TextEncoder().encode(t)).toString("base64");
|
|
51
51
|
}
|
|
52
|
-
function
|
|
53
|
-
return
|
|
52
|
+
function x(t) {
|
|
53
|
+
return t instanceof Error && t.message.includes("Message must be shorter than");
|
|
54
54
|
}
|
|
55
|
-
const
|
|
55
|
+
const R = "eu-west-3", Q = {
|
|
56
56
|
dev: "https://sqs.eu-west-3.amazonaws.com/533547282673/audit-logs-ingest",
|
|
57
57
|
staging: "https://sqs.eu-west-3.amazonaws.com/589883667243/audit-logs-ingest",
|
|
58
58
|
prod: "https://sqs.eu-west-3.amazonaws.com/614948436281/audit-logs-ingest"
|
|
59
|
-
},
|
|
59
|
+
}, T = {
|
|
60
60
|
working: "staging",
|
|
61
61
|
production: "prod",
|
|
62
62
|
development: "dev"
|
|
63
63
|
};
|
|
64
|
-
class
|
|
65
|
-
constructor(e,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.environment = e, this.logger =
|
|
71
|
-
const
|
|
72
|
-
if (this.queueUrl =
|
|
73
|
-
|
|
64
|
+
class F {
|
|
65
|
+
constructor(e, r) {
|
|
66
|
+
n(this, "environment");
|
|
67
|
+
n(this, "logger");
|
|
68
|
+
n(this, "client");
|
|
69
|
+
n(this, "queueUrl");
|
|
70
|
+
this.environment = e, this.logger = r;
|
|
71
|
+
const i = T[e] ?? e;
|
|
72
|
+
if (this.queueUrl = Q[i], !this.queueUrl) {
|
|
73
|
+
r == null || r.warn("audit-logs SDK: no SQS queue URL configured for this environment, audit logs are disabled", { environment: e });
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
|
-
this.client = new
|
|
76
|
+
this.client = new v({ region: R });
|
|
77
77
|
}
|
|
78
78
|
async create(e) {
|
|
79
|
-
var
|
|
79
|
+
var i;
|
|
80
80
|
if (!this.client || !this.queueUrl)
|
|
81
81
|
return { compressed: !1 };
|
|
82
|
-
const
|
|
82
|
+
const r = JSON.stringify(e);
|
|
83
83
|
try {
|
|
84
|
-
return await this.send(
|
|
85
|
-
} catch (
|
|
86
|
-
if (!
|
|
87
|
-
throw
|
|
88
|
-
return (
|
|
84
|
+
return await this.send(r), { compressed: !1 };
|
|
85
|
+
} catch (c) {
|
|
86
|
+
if (!x(c))
|
|
87
|
+
throw c;
|
|
88
|
+
return (i = this.logger) == null || i.info("audit-log payload exceeds the SQS size limit, retrying with Brotli compression", { id: e.id, namespace: e.namespace }), await this.send(N(r)), { compressed: !0 };
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
async send(e) {
|
|
92
|
-
await this.client.send(new
|
|
92
|
+
await this.client.send(new I({
|
|
93
93
|
QueueUrl: this.queueUrl,
|
|
94
94
|
MessageBody: e
|
|
95
95
|
}));
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
class
|
|
99
|
-
constructor(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this.params = e, this.logger = t.logger ?? q(e.service, e.environment), this.transport = t.transport ?? new R(e.environment, this.logger), this.metrics = t.metrics ?? D();
|
|
98
|
+
class o extends Error {
|
|
99
|
+
constructor(r, i) {
|
|
100
|
+
super(`${r}: ${i}`);
|
|
101
|
+
n(this, "path");
|
|
102
|
+
n(this, "reason");
|
|
103
|
+
this.path = r, this.reason = i, this.name = "AuditLogValidationError";
|
|
105
104
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
}
|
|
106
|
+
const P = {
|
|
107
|
+
countryCode: 2,
|
|
108
|
+
internalId: 64,
|
|
109
|
+
namespace: 64,
|
|
110
|
+
service: 64,
|
|
111
|
+
event: 64,
|
|
112
|
+
target: 64,
|
|
113
|
+
environment: 64,
|
|
114
|
+
holdingId: 24,
|
|
115
|
+
companyId: 24,
|
|
116
|
+
employeeId: 24,
|
|
117
|
+
individualId: 24,
|
|
118
|
+
roleId: 24,
|
|
119
|
+
specialRole: 32,
|
|
120
|
+
email: 320
|
|
121
|
+
}, V = /* @__PURE__ */ new Set([
|
|
122
|
+
"namespace",
|
|
123
|
+
"service",
|
|
124
|
+
"event",
|
|
125
|
+
"target",
|
|
126
|
+
"environment"
|
|
127
|
+
]), j = (t) => {
|
|
128
|
+
for (let e = 0; e < t.length; e += 1) {
|
|
129
|
+
const r = t.charCodeAt(e);
|
|
130
|
+
if (r >= 55296 && r <= 56319) {
|
|
131
|
+
if (e + 1 >= t.length)
|
|
132
|
+
return !0;
|
|
133
|
+
const i = t.charCodeAt(e + 1);
|
|
134
|
+
if (i < 56320 || i > 57343)
|
|
135
|
+
return !0;
|
|
136
|
+
e += 1;
|
|
137
|
+
} else if (r >= 56320 && r <= 57343)
|
|
138
|
+
return !0;
|
|
139
|
+
}
|
|
140
|
+
return !1;
|
|
141
|
+
}, f = (t, e, r = {}) => {
|
|
142
|
+
if (t == null) {
|
|
143
|
+
if (r.required)
|
|
144
|
+
throw new o(e, "must not be empty");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (typeof t != "string")
|
|
148
|
+
throw new o(e, "must be a string");
|
|
149
|
+
if (r.required && t === "")
|
|
150
|
+
throw new o(e, "must not be empty");
|
|
151
|
+
if (t.includes("\0"))
|
|
152
|
+
throw new o(e, "contains U+0000");
|
|
153
|
+
if (j(t))
|
|
154
|
+
throw new o(e, "contains unpaired UTF-16 surrogate");
|
|
155
|
+
if (r.limit !== void 0 && Array.from(t).length > r.limit)
|
|
156
|
+
throw new o(e, `exceeds PostgreSQL limit of ${r.limit} characters`);
|
|
157
|
+
}, G = (t, e) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(e) ? `${t}.${e}` : `${t}[key]`, g = (t, e, r) => {
|
|
158
|
+
if (typeof t == "string") {
|
|
159
|
+
f(t, e);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (typeof t == "bigint")
|
|
163
|
+
throw new o(e, "BigInt is not JSON-serializable");
|
|
164
|
+
if (typeof t > "u" || typeof t == "function" || typeof t == "symbol")
|
|
165
|
+
throw new o(e, `${typeof t} is not JSON-serializable`);
|
|
166
|
+
if (typeof t == "number" && !Number.isFinite(t))
|
|
167
|
+
throw new o(e, "must be a finite number");
|
|
168
|
+
if (!(t === null || typeof t != "object")) {
|
|
169
|
+
if (r.has(t))
|
|
170
|
+
throw new o(e, "contains a circular reference");
|
|
171
|
+
r.add(t), Array.isArray(t) ? t.forEach((i, c) => g(i, `${e}[${c}]`, r)) : Object.entries(t).forEach(([i, c]) => {
|
|
172
|
+
f(i, e), g(c, G(e, i), r);
|
|
173
|
+
}), r.delete(t);
|
|
174
|
+
}
|
|
175
|
+
}, J = (t) => {
|
|
176
|
+
for (const [e, r] of Object.entries(P))
|
|
177
|
+
f(t[e], e, {
|
|
178
|
+
limit: r,
|
|
179
|
+
required: V.has(e)
|
|
180
|
+
});
|
|
181
|
+
t.payload !== void 0 && t.payload !== null && g(t.payload, "payload", /* @__PURE__ */ new Set()), t.scope !== void 0 && t.scope !== null && g(t.scope, "scope", /* @__PURE__ */ new Set());
|
|
182
|
+
};
|
|
183
|
+
class X {
|
|
184
|
+
constructor(e, r = {}) {
|
|
185
|
+
n(this, "params");
|
|
186
|
+
n(this, "transport");
|
|
187
|
+
n(this, "logger");
|
|
188
|
+
n(this, "metrics");
|
|
189
|
+
this.params = e, this.logger = r.logger ?? $(e.service, e.environment), this.transport = r.transport ?? new F(e.environment, this.logger), this.metrics = r.metrics ?? D();
|
|
190
|
+
}
|
|
191
|
+
async log({ scope: e, account: r, ...i }) {
|
|
192
|
+
const c = _(), { environment: u, service: h } = this.params, s = {
|
|
193
|
+
...i,
|
|
194
|
+
...r,
|
|
195
|
+
service: h,
|
|
196
|
+
id: c,
|
|
197
|
+
environment: u,
|
|
113
198
|
...e && { scope: e }
|
|
114
|
-
},
|
|
115
|
-
service:
|
|
116
|
-
environment:
|
|
117
|
-
namespace:
|
|
118
|
-
event:
|
|
119
|
-
target:
|
|
120
|
-
},
|
|
199
|
+
}, d = {
|
|
200
|
+
service: h,
|
|
201
|
+
environment: u,
|
|
202
|
+
namespace: s.namespace,
|
|
203
|
+
event: s.event,
|
|
204
|
+
target: s.target
|
|
205
|
+
}, p = Date.now();
|
|
121
206
|
try {
|
|
122
|
-
|
|
207
|
+
J(s);
|
|
208
|
+
const { compressed: l } = await this.transport.create(s);
|
|
123
209
|
this.metrics.increment("sent", {
|
|
124
|
-
...
|
|
125
|
-
compressed: String(
|
|
126
|
-
}), this.metrics.histogram("send_duration_ms", Date.now() -
|
|
127
|
-
id:
|
|
128
|
-
event:
|
|
129
|
-
target:
|
|
210
|
+
...d,
|
|
211
|
+
compressed: String(l)
|
|
212
|
+
}), this.metrics.histogram("send_duration_ms", Date.now() - p, d), this.logger.debug("audit-log sent", {
|
|
213
|
+
id: s.id,
|
|
214
|
+
event: s.event,
|
|
215
|
+
target: s.target
|
|
130
216
|
});
|
|
131
|
-
} catch (
|
|
132
|
-
|
|
133
|
-
|
|
217
|
+
} catch (l) {
|
|
218
|
+
const a = l instanceof o ? l : void 0;
|
|
219
|
+
this.metrics.increment(a ? "validation_failed" : "send_failed", d), this.metrics.histogram("send_duration_ms", Date.now() - p, d);
|
|
220
|
+
const w = l.message;
|
|
134
221
|
try {
|
|
135
|
-
this.logger.error("audit-log write failed", {
|
|
136
|
-
audit_log:
|
|
137
|
-
error:
|
|
222
|
+
this.logger.error(a ? "audit-log validation failed" : "audit-log write failed", {
|
|
223
|
+
audit_log: s,
|
|
224
|
+
error: w,
|
|
225
|
+
...a && {
|
|
226
|
+
validation: {
|
|
227
|
+
path: a.path,
|
|
228
|
+
reason: a.reason
|
|
229
|
+
}
|
|
230
|
+
}
|
|
138
231
|
});
|
|
139
232
|
} catch {
|
|
140
|
-
this.metrics.increment("serialization_failed",
|
|
233
|
+
this.metrics.increment("serialization_failed", d);
|
|
141
234
|
try {
|
|
142
|
-
this.logger.error("audit-log write failed (serialization failed)", {
|
|
235
|
+
this.logger.error(a ? "audit-log validation failed (serialization failed)" : "audit-log write failed (serialization failed)", {
|
|
143
236
|
audit_log_meta: {
|
|
144
|
-
id:
|
|
145
|
-
service:
|
|
146
|
-
environment:
|
|
147
|
-
namespace:
|
|
148
|
-
event:
|
|
149
|
-
target:
|
|
237
|
+
id: s.id,
|
|
238
|
+
service: s.service,
|
|
239
|
+
environment: s.environment,
|
|
240
|
+
namespace: s.namespace,
|
|
241
|
+
event: s.event,
|
|
242
|
+
target: s.target
|
|
150
243
|
},
|
|
151
|
-
error:
|
|
244
|
+
error: w,
|
|
245
|
+
...a && {
|
|
246
|
+
validation: {
|
|
247
|
+
path: a.path,
|
|
248
|
+
reason: a.reason
|
|
249
|
+
}
|
|
250
|
+
}
|
|
152
251
|
});
|
|
153
252
|
} catch {
|
|
154
253
|
}
|
|
@@ -157,5 +256,5 @@ class j {
|
|
|
157
256
|
}
|
|
158
257
|
}
|
|
159
258
|
export {
|
|
160
|
-
|
|
259
|
+
X as AuditLogger
|
|
161
260
|
};
|
package/dist/metrics.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const METRIC_PREFIX = "payfit.audit_logs.";
|
|
2
|
-
export type MetricName = 'sent' | 'send_failed' | 'serialization_failed' | 'send_duration_ms';
|
|
2
|
+
export type MetricName = 'sent' | 'send_failed' | 'validation_failed' | 'serialization_failed' | 'send_duration_ms';
|
|
3
3
|
export interface MetricTags {
|
|
4
4
|
service: string;
|
|
5
5
|
environment: string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { IAuditLogCreationData } from './transport/interface';
|
|
2
|
+
export declare class AuditLogValidationError extends Error {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly reason: string;
|
|
5
|
+
constructor(path: string, reason: string);
|
|
6
|
+
}
|
|
7
|
+
export declare const validateAuditLogForPersistence: (event: IAuditLogCreationData) => void;
|