santoid-sdk 1.0.6 → 2.0.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.
Files changed (59) hide show
  1. package/README.md +113 -29
  2. package/browser/liveness/index.js +1 -0
  3. package/liveness/index.d.ts +9 -0
  4. package/liveness/index.js +29 -0
  5. package/liveness/methods/livenessDetection.d.ts +6 -0
  6. package/{dist/esm/liveness/index.js → liveness/methods/livenessDetection.js} +2 -22
  7. package/liveness/methods/uploadIdentificationDocument.d.ts +91 -0
  8. package/liveness/methods/uploadIdentificationDocument.js +236 -0
  9. package/liveness/utils/index.d.ts +2 -0
  10. package/{dist/esm/liveness → liveness}/utils/index.js +10 -1
  11. package/{dist/esm/liveness → liveness}/utils/types.d.ts +2 -2
  12. package/{dist/node/liveness → liveness}/utils/types.js +1 -1
  13. package/{dist/esm/liveness → liveness}/utils/websocket.js +1 -1
  14. package/{dist/node → node}/auth/index.js +3 -6
  15. package/node/liveness/index.d.ts +9 -0
  16. package/node/liveness/index.js +29 -0
  17. package/node/liveness/methods/livenessDetection.d.ts +6 -0
  18. package/{dist/node/liveness/index.js → node/liveness/methods/livenessDetection.js} +2 -3
  19. package/node/liveness/methods/uploadIdentificationDocument.d.ts +91 -0
  20. package/node/liveness/methods/uploadIdentificationDocument.js +237 -0
  21. package/node/liveness/utils/index.d.ts +2 -0
  22. package/{dist/node → node}/liveness/utils/index.js +10 -1
  23. package/package.json +12 -29
  24. package/tsconfig.json +6 -2
  25. package/tsconfig.prod.json +26 -0
  26. package/{dist/utils → utils}/constants.d.ts +1 -1
  27. package/{dist/utils → utils}/constants.js +1 -1
  28. package/{dist/utils → utils}/types.d.ts +11 -8
  29. package/webpack.config.cjs +12 -2
  30. package/dist/browser/liveness/index.js +0 -1
  31. package/dist/esm/liveness/index.d.ts +0 -9
  32. package/dist/esm/liveness/utils/index.d.ts +0 -1
  33. package/dist/node/liveness/index.d.ts +0 -4
  34. package/dist/node/liveness/utils/index.d.ts +0 -1
  35. package/jest.config.cjs +0 -17
  36. package/src/esm/liveness/index.ts +0 -156
  37. package/src/esm/liveness/utils/getNextFrame.ts +0 -76
  38. package/src/esm/liveness/utils/index.ts +0 -16
  39. package/src/esm/liveness/utils/types.ts +0 -7
  40. package/src/esm/liveness/utils/websocket.ts +0 -33
  41. package/src/index.ts +0 -0
  42. package/src/node/auth/index.ts +0 -43
  43. package/src/node/liveness/index.ts +0 -135
  44. package/src/node/liveness/utils/index.ts +0 -3
  45. package/src/node/liveness/utils/types.ts +0 -8
  46. package/src/node/liveness/utils/websocket.ts +0 -35
  47. package/src/utils/constants.ts +0 -3
  48. package/src/utils/types.ts +0 -46
  49. /package/{dist/index.d.ts → index.d.ts} +0 -0
  50. /package/{dist/index.js → index.js} +0 -0
  51. /package/{dist/esm/liveness → liveness}/utils/getNextFrame.d.ts +0 -0
  52. /package/{dist/esm/liveness → liveness}/utils/getNextFrame.js +0 -0
  53. /package/{dist/esm/liveness → liveness}/utils/websocket.d.ts +0 -0
  54. /package/{dist/node → node}/auth/index.d.ts +0 -0
  55. /package/{dist/node → node}/liveness/utils/types.d.ts +0 -0
  56. /package/{dist/esm → node}/liveness/utils/types.js +0 -0
  57. /package/{dist/node → node}/liveness/utils/websocket.d.ts +0 -0
  58. /package/{dist/node → node}/liveness/utils/websocket.js +0 -0
  59. /package/{dist/utils → utils}/types.js +0 -0
@@ -1,156 +0,0 @@
1
- import { type TPositions, type IWebSocketMessage, type TWebSDKOptions } from './utils/types'
2
- import { SEND_FRAMES_INTERVAL_MS } from '../../utils/constants'
3
- import { builtInGetNextFrame, type builtInGetNextFrameReturn } from './utils/getNextFrame'
4
-
5
- import { connectToWebSocket, disconnectFromWebSocket } from './utils/websocket'
6
-
7
- export * from './utils/types'
8
-
9
- async function onWebSocketMessage (options: TWebSDKOptions, event: MessageEvent) {
10
- const message = event.data
11
-
12
- let messageObj: IWebSocketMessage | undefined
13
-
14
- try {
15
- messageObj = JSON.parse(message)
16
- } catch (err) {
17
- console.error('Cannot parse API message', err)
18
- }
19
-
20
- if (messageObj?.next_step && !messageObj.error) {
21
- await options.nextStepCallback?.(messageObj.next_step)
22
- await options[`${messageObj.next_step}Callback`]?.()
23
- }
24
-
25
- if (messageObj?.error) {
26
- await options.errorCallback?.({ code: 'wrong-position' })
27
- }
28
-
29
- return messageObj
30
- }
31
-
32
- export async function livenessDetection (options: TWebSDKOptions) {
33
- type FrameType = Blob | null | undefined
34
-
35
- let websocketClosedBySDK = false
36
- let builtInGetNextFrameReturn: builtInGetNextFrameReturn | undefined
37
- let validationSuccess = false
38
- let endCalled = false
39
- let canSendNextBytes = true
40
-
41
- const successFrames: Record<TPositions, FrameType> = {
42
- front: undefined,
43
- right: undefined,
44
- left: undefined,
45
- up: undefined,
46
- bottom: undefined,
47
- }
48
-
49
- let lastSentBytes: FrameType
50
- let currentStep: TPositions = 'front'
51
-
52
- async function stopLivenessDetection (options: TWebSDKOptions, websocket?: WebSocket, intervalKey?: any) {
53
- if (intervalKey) {
54
- clearInterval(intervalKey)
55
- }
56
-
57
- if (websocket) {
58
- disconnectFromWebSocket(websocket)
59
- }
60
-
61
- if (builtInGetNextFrameReturn) {
62
- builtInGetNextFrameReturn.stopGetNextFrame()
63
- }
64
-
65
- !endCalled && await options.endCallback?.()
66
- endCalled = true
67
-
68
- websocketClosedBySDK = true
69
- }
70
-
71
- let sendFramesIntervalKey: any
72
- let websocket: WebSocket | undefined
73
-
74
- try {
75
- websocket = await connectToWebSocket(options)
76
-
77
- await options.nextStepCallback?.('front')
78
- await options.frontCallback?.()
79
-
80
- if (!options.getNextFrame && !builtInGetNextFrameReturn) {
81
- builtInGetNextFrameReturn = await builtInGetNextFrame()
82
- }
83
-
84
- sendFramesIntervalKey = setInterval(async () => {
85
- try {
86
- if (canSendNextBytes) {
87
- let bytes
88
-
89
- if (options.getNextFrame) {
90
- bytes = await options.getNextFrame()
91
- } else {
92
- bytes = await builtInGetNextFrameReturn?.getNextFrame()
93
- }
94
-
95
- if (bytes) {
96
- websocket?.send(bytes)
97
- lastSentBytes = bytes
98
- canSendNextBytes = false
99
- }
100
- }
101
- } catch (err) {
102
- await stopLivenessDetection(options, websocket, sendFramesIntervalKey)
103
- }
104
- }, SEND_FRAMES_INTERVAL_MS)
105
-
106
- websocket.addEventListener('message', async (event) => {
107
- const parsedMessage = await onWebSocketMessage(options, event)
108
-
109
- if (parsedMessage) {
110
- canSendNextBytes = !!parsedMessage?.can_send_next_bytes
111
- }
112
-
113
- if (parsedMessage?.validation && currentStep) {
114
- successFrames[currentStep] = lastSentBytes
115
- }
116
-
117
- if (parsedMessage?.next_step) {
118
- currentStep = parsedMessage.next_step
119
- }
120
-
121
- if (parsedMessage?.success) {
122
- successFrames.bottom = lastSentBytes
123
-
124
- validationSuccess = true
125
- await options.successCallback?.<FrameType>({ livenessId: parsedMessage.livenessId, successFrames })
126
- await stopLivenessDetection(options, websocket, sendFramesIntervalKey)
127
- }
128
- })
129
-
130
- websocket.addEventListener('error', async () => {
131
- !validationSuccess && options.errorCallback?.({ code: 'internal-error' })
132
- await stopLivenessDetection(options, websocket, sendFramesIntervalKey)
133
- })
134
-
135
- websocket.addEventListener('close', async () => {
136
- if (!websocketClosedBySDK) {
137
- !validationSuccess && options.errorCallback?.({ code: 'internal-error' })
138
- await stopLivenessDetection(options, websocket, sendFramesIntervalKey)
139
- }
140
- })
141
-
142
- return {
143
- stopLivenessDetection: async () => { await stopLivenessDetection(options, websocket, sendFramesIntervalKey) },
144
- videoStream: builtInGetNextFrameReturn?.videoStream,
145
- }
146
- } catch (err) {
147
- !validationSuccess && options.errorCallback?.({ code: 'internal-error' })
148
- await stopLivenessDetection(options, websocket, sendFramesIntervalKey)
149
- throw err
150
- }
151
- }
152
-
153
- // Class for use in browsers
154
- export default class SantoiDSDK {
155
- static livenessDetection = livenessDetection
156
- }
@@ -1,76 +0,0 @@
1
- export interface builtInGetNextFrameReturn {
2
- getNextFrame: () => Promise<Blob | null>
3
- stopGetNextFrame: () => void
4
- videoStream: MediaStream | undefined
5
- }
6
-
7
- export async function builtInGetNextFrame (): Promise<builtInGetNextFrameReturn> {
8
- let videoStream: MediaStream | undefined
9
- let videoElement: HTMLVideoElement | undefined
10
- let canvasElement: HTMLCanvasElement | undefined
11
- let canvasContext: CanvasRenderingContext2D | null | undefined
12
-
13
- async function getNextFrame (): Promise<Blob | null> {
14
- if (canvasElement && videoElement) {
15
- canvasElement.width = videoElement.videoWidth
16
- canvasElement.height = videoElement.videoHeight
17
-
18
- canvasContext?.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height)
19
-
20
- return await new Promise((resolve, reject) => {
21
- if (!canvasElement) {
22
- resolve(null)
23
- return
24
- }
25
-
26
- canvasElement.toBlob((blob) => {
27
- resolve(blob)
28
- })
29
- })
30
- } else {
31
- throw new Error('Cannot found video element')
32
- }
33
- }
34
-
35
- function stopGetNextFrame () {
36
- if (videoElement) {
37
- document.body.removeChild(videoElement)
38
- videoElement = undefined
39
- }
40
-
41
- if (canvasElement) {
42
- canvasElement = undefined
43
- }
44
-
45
- if (canvasContext) {
46
- canvasContext = undefined
47
- }
48
-
49
- videoStream?.getTracks().forEach(track => { track.stop() })
50
- videoStream = undefined
51
- }
52
-
53
- if (!videoStream || !videoElement || !canvasElement) {
54
- videoStream = await navigator.mediaDevices.getUserMedia({ video: true })
55
-
56
- videoElement = document.createElement('video')
57
- videoElement.setAttribute('autoplay', 'true')
58
-
59
- videoElement.style.transform = 'scale(-1, 1)'
60
- videoElement.style.position = 'absolute'
61
- videoElement.style.left = '99999px'
62
-
63
- document.body.appendChild(videoElement)
64
-
65
- videoElement.srcObject = videoStream
66
-
67
- canvasElement = document.createElement('canvas')
68
- canvasContext = canvasElement.getContext('2d')
69
- }
70
-
71
- return {
72
- getNextFrame,
73
- stopGetNextFrame,
74
- videoStream,
75
- }
76
- }
@@ -1,16 +0,0 @@
1
- export function decodeJwt (token: string) {
2
- const base64Url = token.split('.')[1]
3
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
4
-
5
- const jsonPayload = decodeURIComponent(
6
- window
7
- .atob(base64)
8
- .split('')
9
- .map((c) => {
10
- return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
11
- })
12
- .join(''),
13
- )
14
-
15
- return JSON.parse(jsonPayload)
16
- }
@@ -1,7 +0,0 @@
1
- import { type ISDKOptions } from '../../../utils/types'
2
-
3
- export type TWebSDKOptions = ISDKOptions & {
4
- getNextFrame?: () => Promise<Blob | null>
5
- }
6
-
7
- export * from '../../../utils/types'
@@ -1,33 +0,0 @@
1
- import { decodeJwt } from '.'
2
- import { WEBSOCKET_BASE_URL } from '../../../utils/constants'
3
- import { type IDecodedToken, type TWebSDKOptions } from './types'
4
-
5
- async function waitForWebSocketConnection (websocket: WebSocket) {
6
- await new Promise<void>((resolve) => {
7
- const intervalKey = setInterval(() => {
8
- if ([3, 1].includes(websocket.readyState)) {
9
- clearInterval(intervalKey)
10
- resolve()
11
- }
12
- }, 5)
13
- })
14
-
15
- if (websocket.readyState === 3) {
16
- throw new Error('Cannot connect to server')
17
- }
18
- }
19
-
20
- export async function connectToWebSocket (options: TWebSDKOptions) {
21
- const decodedToken: IDecodedToken = decodeJwt(options.token)
22
- const query = `?x_endpoint_api_userinfo=${options.token}&domain=${options.domain ?? decodedToken.claims.domain}&track=${options.track}&customerRequestId=${options.customId}`
23
-
24
- const websocket = new WebSocket(`${options.customWebSocketUrl ?? WEBSOCKET_BASE_URL}${query}`)
25
- await waitForWebSocketConnection(websocket)
26
- return websocket
27
- }
28
-
29
- export function disconnectFromWebSocket (websocket: WebSocket) {
30
- if (![2, 3].includes(websocket.readyState)) {
31
- websocket.close?.()
32
- }
33
- }
package/src/index.ts DELETED
File without changes
@@ -1,43 +0,0 @@
1
- import fs from 'fs'
2
- import { API_GATEWAY_URL } from '../../utils/constants'
3
-
4
- export interface IInitalizeOptions {
5
- customApiUrl?: string
6
- credentials?: string | Record<string, string>
7
- }
8
-
9
- export async function initialize (options?: IInitalizeOptions) {
10
- let credentialsBase64
11
-
12
- if (options?.credentials) {
13
- if (typeof options?.credentials === 'string') {
14
- credentialsBase64 = options?.credentials
15
- } else if (typeof options?.credentials === 'object') {
16
- credentialsBase64 = Buffer.from(JSON.stringify(options?.credentials)).toString('base64')
17
- } else {
18
- throw new Error('Invalid credentials')
19
- }
20
- } else {
21
- if (process.env.SANTOID_SDK_CREDENTIALS) {
22
- credentialsBase64 = fs.readFileSync(process.env.SANTOID_SDK_CREDENTIALS, { encoding: 'base64' })
23
- } else {
24
- throw new Error('Credentials not found')
25
- }
26
- }
27
-
28
- if (credentialsBase64) {
29
- const response = await fetch(options?.customApiUrl ?? API_GATEWAY_URL, {
30
- method: 'POST',
31
- body: JSON.stringify({ service_account_base64: credentialsBase64 }),
32
- headers: { 'Content-Type': 'application/json' },
33
- })
34
-
35
- const token = await response.json()
36
-
37
- if (typeof token !== 'string') {
38
- throw new Error('Cannot generate token')
39
- }
40
-
41
- return token
42
- }
43
- }
@@ -1,135 +0,0 @@
1
- import type WebSocket from 'ws'
2
- import { type Data } from 'ws'
3
-
4
- import { type TPositions, type IWebSocketMessage, type TNodeSDKOptions } from './utils/types'
5
- import { SEND_FRAMES_INTERVAL_MS } from '../../utils/constants'
6
- import { connectToWebSocket, disconnectFromWebSocket } from './utils/websocket'
7
-
8
- async function onWebSocketMessage (options: TNodeSDKOptions, event: { data: string }) {
9
- const message = event.data
10
-
11
- let messageObj: IWebSocketMessage | undefined
12
-
13
- try {
14
- messageObj = JSON.parse(message)
15
- } catch (err) {
16
- console.error('Cannot parse API message', err)
17
- }
18
-
19
- if (messageObj?.next_step && !messageObj.error) {
20
- await options.nextStepCallback?.(messageObj.next_step)
21
- await options[`${messageObj.next_step}Callback`]?.()
22
- }
23
-
24
- if (messageObj?.error) {
25
- await options.errorCallback?.({ code: 'wrong-position' })
26
- }
27
-
28
- return messageObj
29
- }
30
-
31
- export async function livenessDetection (options: TNodeSDKOptions) {
32
- type FrameType = Data | null | undefined
33
-
34
- let websocketClosedBySDK = false
35
- let validationSuccess = false
36
- let endCalled = false
37
- let canSendNextBytes = true
38
-
39
- const successFrames: Record<TPositions, FrameType> = {
40
- front: undefined,
41
- right: undefined,
42
- left: undefined,
43
- up: undefined,
44
- bottom: undefined,
45
- }
46
-
47
- let lastSentBytes: FrameType
48
- let currentStep: TPositions = 'front'
49
-
50
- async function stopLivenessDetection (options: TNodeSDKOptions, websocket?: WebSocket, intervalKey?: any) {
51
- if (intervalKey) {
52
- clearInterval(intervalKey)
53
- }
54
-
55
- if (websocket) {
56
- disconnectFromWebSocket(websocket)
57
- }
58
-
59
- !endCalled && await options.endCallback?.()
60
- endCalled = true
61
-
62
- websocketClosedBySDK = true
63
- }
64
-
65
- let sendFramesIntervalKey: any
66
- let websocket: WebSocket | undefined
67
-
68
- try {
69
- websocket = await connectToWebSocket(options)
70
-
71
- await options.nextStepCallback?.('front')
72
- await options.frontCallback?.()
73
-
74
- sendFramesIntervalKey = setInterval(async () => {
75
- try {
76
- if (canSendNextBytes) {
77
- const bytes = await options.getNextFrame()
78
-
79
- if (bytes) {
80
- websocket?.send(bytes)
81
- lastSentBytes = bytes
82
- canSendNextBytes = false
83
- }
84
- }
85
- } catch (err) {
86
- await stopLivenessDetection(options, websocket, sendFramesIntervalKey)
87
- }
88
- }, SEND_FRAMES_INTERVAL_MS)
89
-
90
- websocket.on('message', async (data: string) => {
91
- const parsedMessage = await onWebSocketMessage(options, { data: Buffer.from(data).toString() })
92
-
93
- if (parsedMessage) {
94
- canSendNextBytes = !!parsedMessage?.can_send_next_bytes
95
- }
96
-
97
- if (parsedMessage?.validation && currentStep) {
98
- successFrames[currentStep] = lastSentBytes
99
- }
100
-
101
- if (parsedMessage?.next_step) {
102
- currentStep = parsedMessage.next_step
103
- }
104
-
105
- if (parsedMessage?.success) {
106
- successFrames.bottom = lastSentBytes
107
-
108
- validationSuccess = true
109
- await options.successCallback?.<FrameType>({ livenessId: parsedMessage.livenessId, successFrames })
110
- await stopLivenessDetection(options, websocket, sendFramesIntervalKey)
111
- }
112
- })
113
-
114
- websocket.on('error', async (event) => {
115
- !validationSuccess && options.errorCallback?.({ code: 'internal-error' })
116
- await stopLivenessDetection(options, websocket, sendFramesIntervalKey)
117
- })
118
-
119
- websocket.on('close', async () => {
120
- if (!websocketClosedBySDK) {
121
- !validationSuccess && options.errorCallback?.({ code: 'internal-error' })
122
- await stopLivenessDetection(options, websocket, sendFramesIntervalKey)
123
- }
124
- })
125
-
126
- return {
127
- stopLivenessDetection: async () => { await stopLivenessDetection(options, websocket, sendFramesIntervalKey) },
128
- }
129
- } catch (err) {
130
- console.error('catch error', err)
131
- !validationSuccess && options.errorCallback?.({ code: 'internal-error' })
132
- await stopLivenessDetection(options, websocket, sendFramesIntervalKey)
133
- throw err
134
- }
135
- }
@@ -1,3 +0,0 @@
1
- export function decodeJwt (token: string) {
2
- return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString())
3
- }
@@ -1,8 +0,0 @@
1
- import { type Data } from 'ws'
2
- import { type ISDKOptions } from '../../../utils/types'
3
-
4
- export type TNodeSDKOptions = ISDKOptions & {
5
- getNextFrame: () => Promise<Data | null>
6
- }
7
-
8
- export * from '../../../utils/types'
@@ -1,35 +0,0 @@
1
- import WebSocket from 'ws'
2
-
3
- import { decodeJwt } from '.'
4
- import { WEBSOCKET_BASE_URL } from '../../../utils/constants'
5
- import { type IDecodedToken, type TNodeSDKOptions } from './types'
6
-
7
- export async function waitForWebSocketConnection (websocket: WebSocket) {
8
- await new Promise<void>((resolve) => {
9
- const intervalKey = setInterval(() => {
10
- if ([3, 1].includes(websocket.readyState)) {
11
- clearInterval(intervalKey)
12
- resolve()
13
- }
14
- }, 5)
15
- })
16
-
17
- if (websocket.readyState === 3) {
18
- throw new Error('Cannot connect to server')
19
- }
20
- }
21
-
22
- export async function connectToWebSocket (options: TNodeSDKOptions) {
23
- const decodedToken: IDecodedToken = decodeJwt(options.token)
24
- const query = `?x_endpoint_api_userinfo=${options.token}&domain=${options.domain ?? decodedToken.claims.domain}&track=${options.track}&customerRequestId=${options.customId}`
25
-
26
- const websocket = new WebSocket(`${options.customWebSocketUrl ?? WEBSOCKET_BASE_URL}${query}`)
27
- await waitForWebSocketConnection(websocket)
28
- return websocket
29
- }
30
-
31
- export function disconnectFromWebSocket (websocket: WebSocket) {
32
- if (![2, 3].includes(websocket.readyState)) {
33
- websocket.close?.()
34
- }
35
- }
@@ -1,3 +0,0 @@
1
- export const WEBSOCKET_BASE_URL = 'wss://santoid-liveness-w4ynos5dlq-ue.a.run.app/api/v1/liveness/ws'
2
- export const API_GATEWAY_URL = 'https://api.santoid.com.br/api/v1/service-account/refresh-token'
3
- export const SEND_FRAMES_INTERVAL_MS = 100
@@ -1,46 +0,0 @@
1
- export type TCallbackFunction = (...args: any[]) => void | Promise<void>
2
-
3
- export type TPositions = 'front' | 'right' | 'left' | 'up' | 'bottom'
4
-
5
- export type TErrorCode = 'wrong-position' | 'internal-error'
6
-
7
- export interface IDecodedToken {
8
- claims: {
9
- domain: string
10
- }
11
- }
12
-
13
- interface ILivenessResults<FrameType> {
14
- livenessId: string
15
- successFrames: Record<TPositions, FrameType>
16
- }
17
-
18
- export interface ISDKOptions {
19
- token: string
20
- track: string
21
- customId?: string
22
- domain?: string
23
-
24
- nextStepCallback?: (step: TPositions) => void | Promise<void>
25
- frontCallback?: TCallbackFunction
26
- rightCallback?: TCallbackFunction
27
- leftCallback?: TCallbackFunction
28
- upCallback?: TCallbackFunction
29
- bottomCallback?: TCallbackFunction
30
- errorCallback?: (error: { code: TErrorCode }) => void | Promise<void>
31
- successCallback?: <FrameType>(results: ILivenessResults<FrameType>) => void | Promise<void>
32
- endCallback?: TCallbackFunction
33
-
34
- customWebSocketUrl?: string
35
- }
36
-
37
- export interface IWebSocketMessage {
38
- can_send_next_bytes?: boolean
39
- next_step?: TPositions
40
- validation?: boolean
41
-
42
- error?: boolean
43
-
44
- success?: boolean
45
- livenessId: string
46
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes