@payfit/audit-log 3.4.1 → 3.5.0-ephemeral-swpedf-1972.2

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
@@ -16,8 +16,7 @@ Then, you must instanciate a client :
16
16
  import { AuditLogger } from '@payfit/audit-log'
17
17
 
18
18
  const logger = new AuditLogger({
19
- apiUrl: 'http://localhost:8090',
20
- environment: 'an-environment-name',
19
+ environment: 'dev', // must be one of: dev, staging, prod
21
20
  service: 'the-name-of-the-service',
22
21
  })
23
22
  ```
package/dist/index.d.ts CHANGED
@@ -1,7 +1,10 @@
1
+ import type { Logger } from 'winston';
2
+ import { type IMetricsClient } from './metrics';
3
+ import { IAuditLogTransport } from './transport/interface';
1
4
  /**
2
5
  * The allowed Namespaces for the AuditLog
3
6
  */
4
- 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';
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';
5
8
  /**
6
9
  * The allowed targets for the AuditLog
7
10
  */
@@ -92,13 +95,21 @@ export interface IAuditLogger {
92
95
  log: (data: IAuditLogData) => Promise<void>;
93
96
  }
94
97
  export interface IAuditLoggerInitialization {
95
- apiUrl: string;
98
+ /** @deprecated No longer used. The library routes via SQS based on `environment`. Kept for backward compatibility. */
99
+ apiUrl?: string;
96
100
  environment: string;
97
101
  service: string;
98
102
  }
103
+ export interface IAuditLoggerDependencies {
104
+ transport?: IAuditLogTransport;
105
+ logger?: Logger;
106
+ metrics?: IMetricsClient;
107
+ }
99
108
  export declare class AuditLogger implements IAuditLogger {
100
109
  protected params: IAuditLoggerInitialization;
101
- private transport;
102
- constructor(params: IAuditLoggerInitialization);
110
+ private readonly transport;
111
+ private readonly logger;
112
+ private readonly metrics;
113
+ constructor(params: IAuditLoggerInitialization, deps?: IAuditLoggerDependencies);
103
114
  log({ scope, account, ...data }: IAuditLogData): Promise<void>;
104
115
  }
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var d=Object.defineProperty;var P=(s,e,t)=>e in s?d(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t;var n=(s,e,t)=>(P(s,typeof e!="symbol"?e+"":e,t),t);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const m=require("uuid"),h=require("axios"),p="http://localhost:8090".replace(/\/+$/,"");class f{constructor(e,t=p,r=h){n(this,"basePath");n(this,"axios");n(this,"configuration");this.basePath=t,this.axios=r,e&&(this.configuration=e,this.basePath=e.basePath||this.basePath)}}class g extends Error{constructor(t,r){super(r);n(this,"field");n(this,"name","RequiredError");this.field=t}}const A="https://example.com",w=function(s,e,t){if(t==null)throw new g(e,`Required parameter ${e} was null or undefined when calling ${s}.`)},b=function(s,...e){const t=new URLSearchParams(s.search);for(const r of e)for(const o in r)if(Array.isArray(r[o])){t.delete(o);for(const a of r[o])t.append(o,a)}else t.set(o,r[o]);s.search=t.toString()},x=function(s,e,t){const r=typeof s!="string";return(r&&t&&t.isJsonMime?t.isJsonMime(e.headers["Content-Type"]):r)?JSON.stringify(s!==void 0?s:{}):s||""},y=function(s){return s.pathname+s.search+s.hash},L=function(s,e,t,r){return(o=e,a=t)=>{const i={...s.options,url:((r==null?void 0:r.basePath)||a)+s.url};return o.request(i)}},S=function(s){return{v1LogPost:async(e,t={})=>{w("v1LogPost","inlineObject",e);const r="/v1/log",o=new URL(r,A);let a;s&&(a=s.baseOptions);const i={method:"POST",...a,...t},c={},l={};c["Content-Type"]="application/json",b(o,l);let u=a&&a.headers?a.headers:{};return i.headers={...c,...u,...t.headers},i.data=x(e,i,s),{url:y(o),options:i}}}},E=function(s){const e=S(s);return{async v1LogPost(t,r){const o=await e.v1LogPost(t,r);return L(o,h,p,s)}}};class v extends f{v1LogPost(e,t){return E(this.configuration).v1LogPost(e,t).then(r=>r(this.axios,this.basePath))}}const R=s=>"isAxiosError"in s&&s.isAxiosError&&s instanceof Error;class j{constructor(e){n(this,"apiUrl");n(this,"api");this.apiUrl=e,this.api=new v({basePath:this.apiUrl,isJsonMime(t){const r=new RegExp("^(application/json|[^;/ ]+/[^;/ ]+[+]json)[ ]*(;.*)?$","i");return t!==null&&(r.test(t)||t.toLowerCase()==="application/json-patch+json")}})}async create(e){var t;try{await this.api.v1LogPost(e,{timeout:5e3})}catch(r){if(!R(r))throw r;const o=((t=r.response)==null?void 0:t.data)||r.message;throw new Error(o)}}}class q{constructor(e){n(this,"params");n(this,"transport");this.params=e,this.transport=new j(e.apiUrl)}async log({scope:e,account:t,...r}){const o=m.v4(),{environment:a,service:i}=this.params,c=Math.floor(Date.now()/1e3),l={...r,...t,service:i,id:o,environment:a,timestamp:c,...e&&{scope:e}};await this.transport.create(l)}}exports.AuditLogger=q;
1
+ "use strict";var w=Object.defineProperty;var p=(s,t,e)=>t in s?w(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e;var n=(s,t,e)=>(p(s,typeof t!="symbol"?t+"":t,e),e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const S=require("uuid"),o=require("winston"),f=require("hot-shots"),m=require("@aws-sdk/client-sqs"),_="audit-logs",E="@payfit/audit-log",D=()=>{const s=process.env.LOG_LEVEL;return s&&s.trim()!==""?s.trim():"info"},L=(s,t)=>o.createLogger({level:D(),silent:process.env.NODE_ENV==="test",exitOnError:!1,defaultMeta:{namespace:_,sdk:E,service:s,environment:t},format:o.format.combine(o.format.timestamp(),o.format.errors({stack:!0}),o.format.json()),transports:[new o.transports.Console]}),q="payfit.audit-logs.",d=s=>Object.entries(s).filter(([,t])=>t!==void 0&&t!=="").map(([t,e])=>`${t}:${String(e)}`);class A{constructor(){n(this,"client");this.client=new f.StatsD({host:process.env.DD_AGENT_HOST??"localhost",port:Number(process.env.DD_DOGSTATSD_PORT??8125),prefix:q,errorHandler:()=>{},cacheDns:!0})}increment(t,e){this.client.increment(t,1,d(e))}histogram(t,e,i){this.client.histogram(t,e,d(i))}async close(){await new Promise(t=>{this.client.close(()=>t())})}}const y=()=>new A,O="eu-west-3",M={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"},N={working:"staging",production:"prod"};class T{constructor(t){n(this,"environment");n(this,"client");n(this,"queueUrl");this.environment=t;const e=N[t]??t,i=M[e];if(!i)throw new Error(`audit-logs SDK: no SQS queue URL configured for environment "${t}"`);this.queueUrl=i,this.client=new m.SQSClient({region:O})}async create(t){await this.client.send(new m.SendMessageCommand({QueueUrl:this.queueUrl,MessageBody:JSON.stringify(t)}))}}class U{constructor(t,e={}){n(this,"params");n(this,"transport");n(this,"logger");n(this,"metrics");this.params=t,this.transport=e.transport??new T(t.environment),this.logger=e.logger??L(t.service,t.environment),this.metrics=e.metrics??y()}async log({scope:t,account:e,...i}){const h=S.v4(),{environment:c,service:g}=this.params,r={...i,...e,service:g,id:h,environment:c,...t&&{scope:t}},a={service:g,environment:c,namespace:r.namespace,event:r.event,target:r.target},l=Date.now();try{await this.transport.create(r),this.metrics.increment("sent",a),this.metrics.histogram("send_duration_ms",Date.now()-l,a),this.logger.debug("audit-log sent",{id:r.id,event:r.event,target:r.target})}catch(v){this.metrics.increment("send_failed",a),this.metrics.histogram("send_duration_ms",Date.now()-l,a);const u=v.message;try{this.logger.error("audit-log write failed",{audit_log:r,error:u})}catch{this.metrics.increment("serialization_failed",a),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:u})}}}}exports.AuditLogger=U;
package/dist/index.mjs CHANGED
@@ -1,149 +1,130 @@
1
- var u = Object.defineProperty;
2
- var P = (s, e, t) => e in s ? u(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t;
3
- var n = (s, e, t) => (P(s, typeof e != "symbol" ? e + "" : e, t), t);
4
- import { v4 as m } from "uuid";
5
- import l from "axios";
6
- const p = "http://localhost:8090".replace(/\/+$/, "");
7
- class f {
8
- constructor(e, t = p, o = l) {
9
- n(this, "basePath");
10
- n(this, "axios");
11
- n(this, "configuration");
12
- this.basePath = t, this.axios = o, e && (this.configuration = e, this.basePath = e.basePath || this.basePath);
1
+ var p = Object.defineProperty;
2
+ var v = (s, t, e) => t in s ? p(s, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : s[t] = e;
3
+ var n = (s, t, e) => (v(s, typeof t != "symbol" ? t + "" : t, e), e);
4
+ import { v4 as w } from "uuid";
5
+ import { createLogger as f, format as a, transports as _ } from "winston";
6
+ import { StatsD as S } from "hot-shots";
7
+ import { SQSClient as E, SendMessageCommand as D } from "@aws-sdk/client-sqs";
8
+ const L = "audit-logs", A = "@payfit/audit-log", y = () => {
9
+ const s = process.env.LOG_LEVEL;
10
+ return s && s.trim() !== "" ? s.trim() : "info";
11
+ }, O = (s, t) => f({
12
+ level: y(),
13
+ silent: process.env.NODE_ENV === "test",
14
+ exitOnError: !1,
15
+ defaultMeta: {
16
+ namespace: L,
17
+ sdk: A,
18
+ service: s,
19
+ environment: t
20
+ },
21
+ format: a.combine(a.timestamp(), a.errors({ stack: !0 }), a.json()),
22
+ transports: [new _.Console()]
23
+ }), M = "payfit.audit-logs.", d = (s) => Object.entries(s).filter(([, t]) => t !== void 0 && t !== "").map(([t, e]) => `${t}:${String(e)}`);
24
+ class N {
25
+ constructor() {
26
+ n(this, "client");
27
+ this.client = new S({
28
+ host: process.env.DD_AGENT_HOST ?? "localhost",
29
+ port: Number(process.env.DD_DOGSTATSD_PORT ?? 8125),
30
+ prefix: M,
31
+ // Loss of metrics never breaks the caller — DogStatsD UDP is best-effort by design.
32
+ errorHandler: () => {
33
+ },
34
+ cacheDns: !0
35
+ });
13
36
  }
14
- }
15
- class A extends Error {
16
- constructor(t, o) {
17
- super(o);
18
- n(this, "field");
19
- n(this, "name", "RequiredError");
20
- this.field = t;
37
+ increment(t, e) {
38
+ this.client.increment(t, 1, d(e));
21
39
  }
22
- }
23
- const w = "https://example.com", x = function(s, e, t) {
24
- if (t == null)
25
- throw new A(e, `Required parameter ${e} was null or undefined when calling ${s}.`);
26
- }, g = function(s, ...e) {
27
- const t = new URLSearchParams(s.search);
28
- for (const o of e)
29
- for (const r in o)
30
- if (Array.isArray(o[r])) {
31
- t.delete(r);
32
- for (const a of o[r])
33
- t.append(r, a);
34
- } else
35
- t.set(r, o[r]);
36
- s.search = t.toString();
37
- }, b = function(s, e, t) {
38
- const o = typeof s != "string";
39
- return (o && t && t.isJsonMime ? t.isJsonMime(e.headers["Content-Type"]) : o) ? JSON.stringify(s !== void 0 ? s : {}) : s || "";
40
- }, y = function(s) {
41
- return s.pathname + s.search + s.hash;
42
- }, E = function(s, e, t, o) {
43
- return (r = e, a = t) => {
44
- const i = { ...s.options, url: ((o == null ? void 0 : o.basePath) || a) + s.url };
45
- return r.request(i);
46
- };
47
- }, L = function(s) {
48
- return {
49
- /**
50
- *
51
- * @summary Creates Audit Log event
52
- * @param {InlineObject} inlineObject
53
- * @param {*} [options] Override http request option.
54
- * @throws {RequiredError}
55
- */
56
- v1LogPost: async (e, t = {}) => {
57
- x("v1LogPost", "inlineObject", e);
58
- const o = "/v1/log", r = new URL(o, w);
59
- let a;
60
- s && (a = s.baseOptions);
61
- const i = { method: "POST", ...a, ...t }, c = {}, h = {};
62
- c["Content-Type"] = "application/json", g(r, h);
63
- let d = a && a.headers ? a.headers : {};
64
- return i.headers = { ...c, ...d, ...t.headers }, i.data = b(e, i, s), {
65
- url: y(r),
66
- options: i
67
- };
68
- }
69
- };
70
- }, S = function(s) {
71
- const e = L(s);
72
- return {
73
- /**
74
- *
75
- * @summary Creates Audit Log event
76
- * @param {InlineObject} inlineObject
77
- * @param {*} [options] Override http request option.
78
- * @throws {RequiredError}
79
- */
80
- async v1LogPost(t, o) {
81
- const r = await e.v1LogPost(t, o);
82
- return E(r, l, p, s);
83
- }
84
- };
85
- };
86
- class v extends f {
87
- /**
88
- *
89
- * @summary Creates Audit Log event
90
- * @param {InlineObject} inlineObject
91
- * @param {*} [options] Override http request option.
92
- * @throws {RequiredError}
93
- * @memberof DefaultApi
94
- */
95
- v1LogPost(e, t) {
96
- return S(this.configuration).v1LogPost(e, t).then((o) => o(this.axios, this.basePath));
40
+ histogram(t, e, i) {
41
+ this.client.histogram(t, e, d(i));
97
42
  }
98
- }
99
- const R = (s) => "isAxiosError" in s && s.isAxiosError && s instanceof Error;
100
- class U {
101
- constructor(e) {
102
- n(this, "apiUrl");
103
- n(this, "api");
104
- this.apiUrl = e, this.api = new v({
105
- basePath: this.apiUrl,
106
- isJsonMime(t) {
107
- const o = new RegExp(
108
- // eslint-disable-next-line no-control-regex
109
- "^(application/json|[^;/ ]+/[^;/ ]+[+]json)[ ]*(;.*)?$",
110
- "i"
111
- );
112
- return t !== null && (o.test(t) || t.toLowerCase() === "application/json-patch+json");
113
- }
43
+ async close() {
44
+ await new Promise((t) => {
45
+ this.client.close(() => t());
114
46
  });
115
47
  }
116
- async create(e) {
117
- var t;
118
- try {
119
- await this.api.v1LogPost(e, { timeout: 5e3 });
120
- } catch (o) {
121
- if (!R(o))
122
- throw o;
123
- const r = ((t = o.response) == null ? void 0 : t.data) || o.message;
124
- throw new Error(r);
125
- }
48
+ }
49
+ const U = () => new N(), q = "eu-west-3", C = {
50
+ dev: "https://sqs.eu-west-3.amazonaws.com/533547282673/audit-logs-ingest",
51
+ staging: "https://sqs.eu-west-3.amazonaws.com/589883667243/audit-logs-ingest",
52
+ prod: "https://sqs.eu-west-3.amazonaws.com/614948436281/audit-logs-ingest"
53
+ }, T = {
54
+ working: "staging",
55
+ production: "prod"
56
+ };
57
+ class G {
58
+ constructor(t) {
59
+ n(this, "environment");
60
+ n(this, "client");
61
+ n(this, "queueUrl");
62
+ this.environment = t;
63
+ const e = T[t] ?? t, i = C[e];
64
+ if (!i)
65
+ throw new Error(`audit-logs SDK: no SQS queue URL configured for environment "${t}"`);
66
+ this.queueUrl = i, this.client = new E({ region: q });
67
+ }
68
+ async create(t) {
69
+ await this.client.send(new D({
70
+ QueueUrl: this.queueUrl,
71
+ MessageBody: JSON.stringify(t)
72
+ }));
126
73
  }
127
74
  }
128
- class O {
129
- constructor(e) {
75
+ class P {
76
+ constructor(t, e = {}) {
130
77
  n(this, "params");
131
78
  n(this, "transport");
132
- this.params = e, this.transport = new U(e.apiUrl);
79
+ n(this, "logger");
80
+ n(this, "metrics");
81
+ this.params = t, this.transport = e.transport ?? new G(t.environment), this.logger = e.logger ?? O(t.service, t.environment), this.metrics = e.metrics ?? U();
133
82
  }
134
- async log({ scope: e, account: t, ...o }) {
135
- const r = m(), { environment: a, service: i } = this.params, c = Math.floor(Date.now() / 1e3), h = {
136
- ...o,
137
- ...t,
138
- service: i,
139
- id: r,
140
- environment: a,
141
- timestamp: c,
142
- ...e && { scope: e }
143
- };
144
- await this.transport.create(h);
83
+ async log({ scope: t, account: e, ...i }) {
84
+ const u = w(), { environment: c, service: g } = this.params, r = {
85
+ ...i,
86
+ ...e,
87
+ service: g,
88
+ id: u,
89
+ environment: c,
90
+ ...t && { scope: t }
91
+ }, o = {
92
+ service: g,
93
+ environment: c,
94
+ namespace: r.namespace,
95
+ event: r.event,
96
+ target: r.target
97
+ }, l = Date.now();
98
+ try {
99
+ await this.transport.create(r), this.metrics.increment("sent", o), this.metrics.histogram("send_duration_ms", Date.now() - l, o), this.logger.debug("audit-log sent", {
100
+ id: r.id,
101
+ event: r.event,
102
+ target: r.target
103
+ });
104
+ } catch (h) {
105
+ this.metrics.increment("send_failed", o), this.metrics.histogram("send_duration_ms", Date.now() - l, o);
106
+ const m = h.message;
107
+ try {
108
+ this.logger.error("audit-log write failed", {
109
+ audit_log: r,
110
+ error: m
111
+ });
112
+ } catch {
113
+ this.metrics.increment("serialization_failed", o), this.logger.error("audit-log write failed (serialization failed)", {
114
+ audit_log_meta: {
115
+ id: r.id,
116
+ service: r.service,
117
+ environment: r.environment,
118
+ namespace: r.namespace,
119
+ event: r.event,
120
+ target: r.target
121
+ },
122
+ error: m
123
+ });
124
+ }
125
+ }
145
126
  }
146
127
  }
147
128
  export {
148
- O as AuditLogger
129
+ P as AuditLogger
149
130
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { type Logger } from 'winston';
2
+ export declare const createSdkLogger: (service: string, environment: string) => Logger;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ export declare const METRIC_PREFIX = "payfit.audit-logs.";
2
+ export type MetricName = 'sent' | 'send_failed' | 'serialization_failed' | 'send_duration_ms';
3
+ export interface MetricTags {
4
+ service: string;
5
+ environment: string;
6
+ namespace?: string;
7
+ event?: string;
8
+ target?: string;
9
+ }
10
+ export interface IMetricsClient {
11
+ increment(name: MetricName, tags: MetricTags): void;
12
+ histogram(name: MetricName, value: number, tags: MetricTags): void;
13
+ close(): Promise<void>;
14
+ }
15
+ export declare const createMetricsClient: () => IMetricsClient;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,8 @@
1
- import { InlineObject } from './generated/model';
2
- import { IAuditLogTransport } from './interface';
1
+ import { IAuditLogCreationData, IAuditLogTransport } from './interface';
3
2
  export declare class Transport implements IAuditLogTransport {
4
- protected apiUrl: string;
5
- private api;
6
- constructor(apiUrl: string);
7
- create(auditLogData: InlineObject): Promise<void>;
3
+ protected environment: string;
4
+ private client;
5
+ private queueUrl;
6
+ constructor(environment: string);
7
+ create(auditLogData: IAuditLogCreationData): Promise<void>;
8
8
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,5 @@
1
1
  export interface IAuditLogCreationData {
2
2
  id: string;
3
- timestamp: number;
4
3
  environment: string;
5
4
  service: string;
6
5
  scope?: Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payfit/audit-log",
3
- "version": "3.4.1",
3
+ "version": "3.5.0-ephemeral-swpedf-1972.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/PayFit/audit-logs.git",
@@ -15,6 +15,8 @@
15
15
  "build": "rimraf dist && vite build",
16
16
  "prepack": "npm run build",
17
17
  "lint": "eslint .",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
18
20
  "start:playground": "ts-node src/playground.ts",
19
21
  "format:check": "prettier --check .",
20
22
  "format:fix": "prettier --write .",
@@ -23,9 +25,12 @@
23
25
  "author": "",
24
26
  "license": "ISC",
25
27
  "dependencies": {
28
+ "@aws-sdk/client-sqs": "^3.600.0",
26
29
  "axios": "^1.0.0",
30
+ "hot-shots": "^11.0.0",
27
31
  "lodash": "^4.17.20",
28
- "uuid": "^9.0.0"
32
+ "uuid": "^9.0.0",
33
+ "winston": "^3.13.0"
29
34
  },
30
35
  "devDependencies": {
31
36
  "@openapitools/openapi-generator-cli": "2.7.0",
@@ -46,13 +51,14 @@
46
51
  "semantic-release": "22.0.12",
47
52
  "ts-node": "10.9.2",
48
53
  "typescript": "5.3.3",
49
- "vite": "4.5.1"
54
+ "vite": "4.5.1",
55
+ "vitest": "4.1.4"
50
56
  },
51
- "peerDependencies": {},
52
57
  "publishConfig": {
53
58
  "registry": "https://registry.npmjs.org/"
54
59
  },
55
60
  "files": [
56
61
  "dist"
57
- ]
62
+ ],
63
+ "packageManager": "yarn@4.14.1+sha512.64df448055b2d37ba269d7db535a469b8da93f8ef1140c25fd7a83c00a8fbaacb214ca0e02553b92a2c54cef78bb67d0b4817fab02001df0e24fac0faccc3b42"
58
64
  }