@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 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, you must instanciate a client :
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
- For now, no unit tests have been written, but it should arrive soon !
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 he SDK
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 w=Object.defineProperty;var f=(s,e,t)=>e in s?w(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t;var i=(s,e,t)=>f(s,typeof e!="symbol"?e+"":e,t);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const v=require("uuid"),o=require("winston"),S=require("hot-shots"),h=require("@aws-sdk/client-sqs"),E=require("zlib"),y="audit-logs",_="@payfit/audit-log",q=()=>{const s=process.env.LOG_LEVEL;return s&&s.trim()!==""?s.trim():"info"},L=(s,e)=>o.createLogger({level:q(),silent:process.env.NODE_ENV==="test",exitOnError:!1,defaultMeta:{namespace:y,sdk:_,service:s,environment:e},format:o.format.combine(o.format.timestamp(),o.format.errors({stack:!0}),o.format.json()),transports:[new o.transports.Console]}),M="payfit.audit_logs.",p=s=>Object.entries(s).filter(([,e])=>e!==void 0&&e!=="").map(([e,t])=>`${e}:${String(t)}`);class b{constructor(){i(this,"client");this.client=new S.StatsD({prefix:M,errorHandler:()=>{},cacheDns:!0})}increment(e,t){this.client.increment(e,1,p(t))}histogram(e,t,n){this.client.histogram(e,t,p(n))}async close(){await new Promise(e=>{this.client.close(()=>e())})}}const A=()=>new b;function U(s){return E.brotliCompressSync(new TextEncoder().encode(s)).toString("base64")}function C(s){return s instanceof Error&&s.message.includes("Message must be shorter than")}const O="eu-west-3",z={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"},D={working:"staging",production:"prod",development:"dev"};class N{constructor(e,t){i(this,"environment");i(this,"logger");i(this,"client");i(this,"queueUrl");this.environment=e,this.logger=t;const n=D[e]??e;if(this.queueUrl=z[n],!this.queueUrl){t==null||t.warn("audit-logs SDK: no SQS queue URL configured for this environment, audit logs are disabled",{environment:e});return}this.client=new h.SQSClient({region:O})}async create(e){var n;if(!this.client||!this.queueUrl)return{compressed:!1};const t=JSON.stringify(e);try{return await this.send(t),{compressed:!1}}catch(c){if(!C(c))throw c;return(n=this.logger)==null||n.info("audit-log payload exceeds the SQS size limit, retrying with Brotli compression",{id:e.id,namespace:e.namespace}),await this.send(U(t)),{compressed:!0}}}async send(e){await this.client.send(new h.SendMessageCommand({QueueUrl:this.queueUrl,MessageBody:e}))}}class T{constructor(e,t={}){i(this,"params");i(this,"transport");i(this,"logger");i(this,"metrics");this.params=e,this.logger=t.logger??L(e.service,e.environment),this.transport=t.transport??new N(e.environment,this.logger),this.metrics=t.metrics??A()}async log({scope:e,account:t,...n}){const c=v.v4(),{environment:g,service:d}=this.params,r={...n,...t,service:d,id:c,environment:g,...e&&{scope:e}},a={service:d,environment:g,namespace:r.namespace,event:r.event,target:r.target},u=Date.now();try{const{compressed:l}=await this.transport.create(r);this.metrics.increment("sent",{...a,compressed:String(l)}),this.metrics.histogram("send_duration_ms",Date.now()-u,a),this.logger.debug("audit-log sent",{id:r.id,event:r.event,target:r.target})}catch(l){this.metrics.increment("send_failed",a),this.metrics.histogram("send_duration_ms",Date.now()-u,a);const m=l.message;try{this.logger.error("audit-log write failed",{audit_log:r,error:m})}catch{this.metrics.increment("serialization_failed",a);try{this.logger.error("audit-log write failed (serialization failed)",{audit_log_meta:{id:r.id,service:r.service,environment:r.environment,namespace:r.namespace,event:r.event,target:r.target},error:m})}catch{}}}}}exports.AuditLogger=T;
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 p = Object.defineProperty;
2
- var f = (s, e, t) => e in s ? p(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t;
3
- var i = (s, e, t) => f(s, typeof e != "symbol" ? e + "" : e, t);
4
- import { v4 as w } from "uuid";
5
- import { createLogger as v, transports as S, format as c } from "winston";
6
- import { StatsD as E } from "hot-shots";
7
- import { SQSClient as _, SendMessageCommand as y } from "@aws-sdk/client-sqs";
8
- import { brotliCompressSync as L } from "zlib";
9
- const M = "audit-logs", A = "@payfit/audit-log", U = () => {
10
- const s = process.env.LOG_LEVEL;
11
- return s && s.trim() !== "" ? s.trim() : "info";
12
- }, q = (s, e) => v({
13
- level: U(),
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: M,
18
- sdk: A,
19
- service: s,
17
+ namespace: q,
18
+ sdk: C,
19
+ service: t,
20
20
  environment: e
21
21
  },
22
- format: c.combine(c.timestamp(), c.errors({ stack: !0 }), c.json()),
23
- transports: [new S.Console()]
24
- }), C = "payfit.audit_logs.", h = (s) => Object.entries(s).filter(([, e]) => e !== void 0 && e !== "").map(([e, t]) => `${e}:${String(t)}`);
25
- class b {
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
- i(this, "client");
28
- this.client = new E({
29
- prefix: C,
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, t) {
37
- this.client.increment(e, 1, h(t));
36
+ increment(e, r) {
37
+ this.client.increment(e, 1, y(r));
38
38
  }
39
- histogram(e, t, n) {
40
- this.client.histogram(e, t, h(n));
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 b();
49
- function N(s) {
50
- return L(new TextEncoder().encode(s)).toString("base64");
48
+ const D = () => new O();
49
+ function N(t) {
50
+ return U(new TextEncoder().encode(t)).toString("base64");
51
51
  }
52
- function O(s) {
53
- return s instanceof Error && s.message.includes("Message must be shorter than");
52
+ function x(t) {
53
+ return t instanceof Error && t.message.includes("Message must be shorter than");
54
54
  }
55
- const z = "eu-west-3", x = {
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
- }, Q = {
59
+ }, T = {
60
60
  working: "staging",
61
61
  production: "prod",
62
62
  development: "dev"
63
63
  };
64
- class R {
65
- constructor(e, t) {
66
- i(this, "environment");
67
- i(this, "logger");
68
- i(this, "client");
69
- i(this, "queueUrl");
70
- this.environment = e, this.logger = t;
71
- const n = Q[e] ?? e;
72
- if (this.queueUrl = x[n], !this.queueUrl) {
73
- t == null || t.warn("audit-logs SDK: no SQS queue URL configured for this environment, audit logs are disabled", { environment: e });
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 _({ region: z });
76
+ this.client = new v({ region: R });
77
77
  }
78
78
  async create(e) {
79
- var n;
79
+ var i;
80
80
  if (!this.client || !this.queueUrl)
81
81
  return { compressed: !1 };
82
- const t = JSON.stringify(e);
82
+ const r = JSON.stringify(e);
83
83
  try {
84
- return await this.send(t), { compressed: !1 };
85
- } catch (a) {
86
- if (!O(a))
87
- throw a;
88
- return (n = this.logger) == null || n.info("audit-log payload exceeds the SQS size limit, retrying with Brotli compression", { id: e.id, namespace: e.namespace }), await this.send(N(t)), { compressed: !0 };
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 y({
92
+ await this.client.send(new I({
93
93
  QueueUrl: this.queueUrl,
94
94
  MessageBody: e
95
95
  }));
96
96
  }
97
97
  }
98
- class j {
99
- constructor(e, t = {}) {
100
- i(this, "params");
101
- i(this, "transport");
102
- i(this, "logger");
103
- i(this, "metrics");
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
- async log({ scope: e, account: t, ...n }) {
107
- const a = w(), { environment: l, service: g } = this.params, r = {
108
- ...n,
109
- ...t,
110
- service: g,
111
- id: a,
112
- environment: l,
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
- }, o = {
115
- service: g,
116
- environment: l,
117
- namespace: r.namespace,
118
- event: r.event,
119
- target: r.target
120
- }, d = Date.now();
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
- const { compressed: m } = await this.transport.create(r);
207
+ J(s);
208
+ const { compressed: l } = await this.transport.create(s);
123
209
  this.metrics.increment("sent", {
124
- ...o,
125
- compressed: String(m)
126
- }), this.metrics.histogram("send_duration_ms", Date.now() - d, o), this.logger.debug("audit-log sent", {
127
- id: r.id,
128
- event: r.event,
129
- target: r.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 (m) {
132
- this.metrics.increment("send_failed", o), this.metrics.histogram("send_duration_ms", Date.now() - d, o);
133
- const u = m.message;
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: r,
137
- error: u
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", o);
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: r.id,
145
- service: r.service,
146
- environment: r.environment,
147
- namespace: r.namespace,
148
- event: r.event,
149
- target: r.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: u
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
- j as AuditLogger
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payfit/audit-log",
3
- "version": "3.17.11",
3
+ "version": "3.18.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/PayFit/audit-logs.git",