@opengovsg/mockpass 4.0.12 → 4.3.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.
@@ -11,10 +11,12 @@ jobs:
11
11
  publish-npm:
12
12
  runs-on: ubuntu-latest
13
13
  steps:
14
- - uses: actions/checkout@v2
15
- - uses: actions/setup-node@v1
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v3
16
16
  with:
17
- node-version: 12
17
+ node-version: 'lts/*'
18
+ cache: 'npm'
19
+ cache-dependency-path: '**/package-lock.json'
18
20
  registry-url: https://registry.npmjs.org/
19
21
  - run: npm ci
20
22
  - run: npm publish --access public
@@ -0,0 +1,6 @@
1
+ // Skip Husky install in production and CI
2
+ if (process.env.NODE_ENV === 'production' || process.env.CI === 'true') {
3
+ process.exit(0)
4
+ }
5
+ const husky = (await import('husky')).default
6
+ console.log(husky())
package/.husky/pre-commit CHANGED
@@ -1,4 +1 @@
1
- #!/bin/sh
2
- . "$(dirname "$0")/_/husky.sh"
3
-
4
1
  npx lint-staged
package/.husky/pre-push CHANGED
@@ -1,4 +1 @@
1
- #!/bin/sh
2
- . "$(dirname "$0")/_/husky.sh"
3
-
4
1
  npx commitlint --from origin/main --to HEAD --verbose
package/Dockerfile CHANGED
@@ -4,6 +4,8 @@ WORKDIR /usr/src/mockpass
4
4
 
5
5
  COPY package* ./
6
6
 
7
+ COPY ./.husky ./.husky
8
+
7
9
  RUN npm ci
8
10
 
9
11
  COPY . ./
package/README.md CHANGED
@@ -84,7 +84,7 @@ Configure your application (or MockPass) with certificates/keys:
84
84
  MockPass accepts any value for `client_id`, `redirect_uri` and `sp_esvcId`.
85
85
  The `client_secret` value will be checked if configured, see below.
86
86
 
87
- Only the profiles (NRICs) that have entries in Mockpass' static dataset will
87
+ Only the profiles (NRICs) that have entries in Mockpass' personas dataset will
88
88
  succeed, using other NRICs will result in an error. See the list of personas in
89
89
  [static/myinfo/v3.json](static/myinfo/v3.json).
90
90
 
@@ -119,10 +119,19 @@ Configure your application (or MockPass) with certificates/keys:
119
119
 
120
120
  MockPass accepts any value for `client_id`, `client_secret` and `redirect_uri`.
121
121
 
122
- Only the profiles (NRICs) that have entries in Mockpass' static dataset will
122
+ Only the profiles (NRICs) that have entries in Mockpass' personas dataset will
123
123
  succeed, using other NRICs will result in an error. See the list of personas in
124
124
  [static/myinfo/v3.json](static/myinfo/v3.json).
125
125
 
126
+ If the Public Officer Employment Details data item is requested, the
127
+ `pocdex.public_officer_details` scope data is sourced from the
128
+ `publicofficerdetails` data key (where present) on personas.
129
+ Most personas do not have this data key configured, and will result in a `"NA"`
130
+ response instead of an stringified array. As these personas are not identified
131
+ in the login page dropdown, please check the personas dataset linked above to
132
+ identify them.
133
+ The `pocdex.number_of_employments` scope is not supported.
134
+
126
135
  | Configuration item | Explanation |
127
136
  |---|---|
128
137
  | Client certificate | **Overview:** When client makes any request, what certificate is used to verify the request signature, and what certificate is used to encrypt the data payload. <br> **Default:** static key `static/certs/key.pub` is used. <br> **How to configure:** Set the env var `SERVICE_PROVIDER_PUB_KEY` to the path to a public key PEM file. (A certificate PEM file can also be provided, despite the env var name.) |
@@ -159,6 +168,7 @@ Common configuration:
159
168
  | Configuration item | Explanation |
160
169
  |---|---|
161
170
  | Port number | **Overview:** What port number MockPass will listen for HTTP requests on. <br> **Default:** 5156. <br> **How to configure:** Set the env var `MOCKPASS_PORT` or `PORT` to some port number. |
171
+ | Stateless Mode | **Overview:** Enable for environments where the state of the process is not guaranteed, such as in serverless contexts. <br> **Default:** not set. <br> **How to configure:** Set the env var `MOCKPASS_STATELESS` to `true` or `false`. |
162
172
 
163
173
  Run MockPass:
164
174
 
package/app.js CHANGED
@@ -35,12 +35,15 @@ const cryptoConfig = {
35
35
  process.env.RESOLVE_ARTIFACT_REQUEST_SIGNED !== 'false',
36
36
  }
37
37
 
38
+ const isStateless = process.env.MOCKPASS_STATELESS === 'true'
39
+
38
40
  const options = {
39
41
  serviceProvider,
40
42
  showLoginPage: (req) =>
41
43
  (req.header('X-Show-Login-Page') || process.env.SHOW_LOGIN_PAGE) === 'true',
42
44
  encryptMyInfo: process.env.ENCRYPT_MYINFO === 'true',
43
45
  cryptoConfig,
46
+ isStateless,
44
47
  }
45
48
 
46
49
  const app = express()
@@ -50,7 +53,7 @@ configOIDC(app, options)
50
53
  configOIDCv2(app, options)
51
54
  configSGID(app, options)
52
55
 
53
- configMyInfo.consent(app)
56
+ configMyInfo.consent(app, options)
54
57
  configMyInfo.v3(app, options)
55
58
 
56
59
  app.enable('trust proxy')
package/lib/auth-code.js CHANGED
@@ -4,14 +4,24 @@ const crypto = require('crypto')
4
4
  const AUTH_CODE_TIMEOUT = 5 * 60 * 1000
5
5
  const profileAndNonceStore = new ExpiryMap(AUTH_CODE_TIMEOUT)
6
6
 
7
- const generateAuthCode = ({ profile, scopes, nonce }) => {
8
- const authCode = crypto.randomBytes(45).toString('base64')
7
+ const generateAuthCode = (
8
+ { profile, scopes, nonce },
9
+ { isStateless = false },
10
+ ) => {
11
+ const authCode = isStateless
12
+ ? Buffer.from(JSON.stringify({ profile, scopes, nonce })).toString(
13
+ 'base64url',
14
+ )
15
+ : crypto.randomBytes(45).toString('base64')
16
+
9
17
  profileAndNonceStore.set(authCode, { profile, scopes, nonce })
10
18
  return authCode
11
19
  }
12
20
 
13
- const lookUpByAuthCode = (authCode) => {
14
- return profileAndNonceStore.get(authCode)
21
+ const lookUpByAuthCode = (authCode, { isStateless = false }) => {
22
+ return isStateless
23
+ ? JSON.parse(Buffer.from(authCode, 'base64url').toString('utf-8'))
24
+ : profileAndNonceStore.get(authCode)
15
25
  }
16
26
 
17
27
  module.exports = { generateAuthCode, lookUpByAuthCode }
@@ -47,13 +47,13 @@ const authorizeViaOIDC = authorize(
47
47
  `/singpass/authorize?client_id=MYINFO-CONSENTPLATFORM&redirect_uri=${MYINFO_ASSERT_ENDPOINT}&state=${state}`,
48
48
  )
49
49
 
50
- function config(app) {
50
+ function config(app, { isStateless }) {
51
51
  app.get(MYINFO_ASSERT_ENDPOINT, (req, res) => {
52
52
  const rawArtifact = req.query.SAMLart || req.query.code
53
53
  const artifact = rawArtifact.replace(/ /g, '+')
54
54
  const state = req.query.RelayState || req.query.state
55
55
 
56
- const profile = lookUpByAuthCode(artifact).profile
56
+ const profile = lookUpByAuthCode(artifact, { isStateless }).profile
57
57
  const myinfoVersion = 'v3'
58
58
 
59
59
  const { nric: id } = profile
@@ -24,7 +24,7 @@ const signingPem = fs.readFileSync(
24
24
  path.resolve(__dirname, '../../../static/certs/spcp-key.pem'),
25
25
  )
26
26
 
27
- function config(app, { showLoginPage, serviceProvider }) {
27
+ function config(app, { showLoginPage, serviceProvider, isStateless }) {
28
28
  for (const idp of ['singPass', 'corpPass']) {
29
29
  const profiles = assertions.oidc[idp]
30
30
  const defaultProfile =
@@ -34,7 +34,7 @@ function config(app, { showLoginPage, serviceProvider }) {
34
34
  const { redirect_uri: redirectURI, state, nonce } = req.query
35
35
  if (showLoginPage(req)) {
36
36
  const values = profiles.map((profile) => {
37
- const authCode = generateAuthCode({ profile, nonce })
37
+ const authCode = generateAuthCode({ profile, nonce }, { isStateless })
38
38
  const assertURL = buildAssertURL(redirectURI, authCode, state)
39
39
  const id = idGenerator[idp](profile)
40
40
  return { id, assertURL }
@@ -53,7 +53,7 @@ function config(app, { showLoginPage, serviceProvider }) {
53
53
  res.send(response)
54
54
  } else {
55
55
  const profile = customProfileFromHeaders[idp](req) || defaultProfile
56
- const authCode = generateAuthCode({ profile, nonce })
56
+ const authCode = generateAuthCode({ profile, nonce }, { isStateless })
57
57
  const assertURL = buildAssertURL(redirectURI, authCode, state)
58
58
  console.warn(
59
59
  `Redirecting login from ${req.query.client_id} to ${redirectURI}`,
@@ -72,7 +72,7 @@ function config(app, { showLoginPage, serviceProvider }) {
72
72
  profile.uen = uen
73
73
  }
74
74
 
75
- const authCode = generateAuthCode({ profile, nonce })
75
+ const authCode = generateAuthCode({ profile, nonce }, { isStateless })
76
76
  const assertURL = buildAssertURL(redirectURI, authCode, state)
77
77
  res.redirect(assertURL)
78
78
  })
@@ -88,20 +88,32 @@ function config(app, { showLoginPage, serviceProvider }) {
88
88
  const { refresh_token: suppliedRefreshToken } = req.body
89
89
  console.warn(`Refreshing tokens with ${suppliedRefreshToken}`)
90
90
 
91
- profile = profileStore.get(suppliedRefreshToken)
91
+ profile = isStateless
92
+ ? JSON.parse(
93
+ Buffer.from(suppliedRefreshToken, 'base64url').toString(
94
+ 'utf-8',
95
+ ),
96
+ )
97
+ : profileStore.get(suppliedRefreshToken)
92
98
  } else {
93
99
  const { code: authCode } = req.body
94
100
  console.warn(
95
101
  `Received auth code ${authCode} from ${aud} and ${req.body.redirect_uri}`,
96
102
  )
97
- ;({ profile, nonce } = lookUpByAuthCode(authCode))
103
+ ;({ profile, nonce } = lookUpByAuthCode(authCode, { isStateless }))
98
104
  }
99
105
 
100
106
  const iss = `${req.protocol}://${req.get('host')}`
101
107
 
102
- const { idTokenClaims, accessToken, refreshToken } =
103
- await assertions.oidc.create[idp](profile, iss, aud, nonce)
108
+ const {
109
+ idTokenClaims,
110
+ accessToken,
111
+ refreshToken: generatedRefreshToken,
112
+ } = await assertions.oidc.create[idp](profile, iss, aud, nonce)
104
113
 
114
+ const refreshToken = isStateless
115
+ ? Buffer.from(JSON.stringify(profile)).toString('base64url')
116
+ : generatedRefreshToken
105
117
  profileStore.set(refreshToken, profile)
106
118
 
107
119
  const signingKey = await jose.JWK.asKey(signingPem, 'pem')
@@ -140,7 +140,7 @@ function findEncryptionKey(jwks, algs) {
140
140
  }
141
141
  }
142
142
 
143
- function config(app, { showLoginPage }) {
143
+ function config(app, { showLoginPage, isStateless }) {
144
144
  for (const idp of ['singPass', 'corpPass']) {
145
145
  const profiles = assertions.oidc[idp]
146
146
  const defaultProfile =
@@ -196,7 +196,7 @@ function config(app, { showLoginPage }) {
196
196
  // Identical to OIDC v1
197
197
  if (showLoginPage(req)) {
198
198
  const values = profiles.map((profile) => {
199
- const authCode = generateAuthCode({ profile, nonce })
199
+ const authCode = generateAuthCode({ profile, nonce }, { isStateless })
200
200
  const assertURL = buildAssertURL(redirectURI, authCode, state)
201
201
  const id = idGenerator[idp](profile)
202
202
  return { id, assertURL }
@@ -215,7 +215,7 @@ function config(app, { showLoginPage }) {
215
215
  res.send(response)
216
216
  } else {
217
217
  const profile = customProfileFromHeaders[idp](req) || defaultProfile
218
- const authCode = generateAuthCode({ profile, nonce })
218
+ const authCode = generateAuthCode({ profile, nonce }, { isStateless })
219
219
  const assertURL = buildAssertURL(redirectURI, authCode, state)
220
220
  console.warn(
221
221
  `Redirecting login from ${req.query.client_id} to ${redirectURI}`,
@@ -234,7 +234,7 @@ function config(app, { showLoginPage }) {
234
234
  profile.uen = uen
235
235
  }
236
236
 
237
- const authCode = generateAuthCode({ profile, nonce })
237
+ const authCode = generateAuthCode({ profile, nonce }, { isStateless })
238
238
  const assertURL = buildAssertURL(redirectURI, authCode, state)
239
239
  res.redirect(assertURL)
240
240
  })
@@ -434,7 +434,7 @@ function config(app, { showLoginPage }) {
434
434
  }
435
435
 
436
436
  // Step 1: Obtain profile for which the auth code requested data for
437
- const { profile, nonce } = lookUpByAuthCode(authCode)
437
+ const { profile, nonce } = lookUpByAuthCode(authCode, { isStateless })
438
438
 
439
439
  // Step 2: Get ID token
440
440
  const aud = clientAssertionClaims['sub']
@@ -30,7 +30,7 @@ const buildAssertURL = (redirectURI, authCode, state) =>
30
30
  authCode,
31
31
  )}&state=${encodeURIComponent(state)}`
32
32
 
33
- function config(app, { showLoginPage, serviceProvider }) {
33
+ function config(app, { showLoginPage, serviceProvider, isStateless }) {
34
34
  const profiles = assertions.oidc.singPass
35
35
  const defaultProfile =
36
36
  profiles.find((p) => p.nric === process.env.MOCKPASS_NRIC) || profiles[0]
@@ -43,7 +43,10 @@ function config(app, { showLoginPage, serviceProvider }) {
43
43
  const values = profiles
44
44
  .filter((profile) => assertions.myinfo.v3.personas[profile.nric])
45
45
  .map((profile) => {
46
- const authCode = generateAuthCode({ profile, scopes, nonce })
46
+ const authCode = generateAuthCode(
47
+ { profile, scopes, nonce },
48
+ { isStateless },
49
+ )
47
50
  const assertURL = buildAssertURL(redirectURI, authCode, state)
48
51
  const id = idGenerator.singPass(profile)
49
52
  return { id, assertURL }
@@ -52,7 +55,10 @@ function config(app, { showLoginPage, serviceProvider }) {
52
55
  res.send(response)
53
56
  } else {
54
57
  const profile = defaultProfile
55
- const authCode = generateAuthCode({ profile, scopes, nonce })
58
+ const authCode = generateAuthCode(
59
+ { profile, scopes, nonce },
60
+ { isStateless },
61
+ )
56
62
  const assertURL = buildAssertURL(redirectURI, authCode, state)
57
63
  console.info(
58
64
  `Redirecting login from ${req.query.client_id} to ${assertURL}`,
@@ -74,7 +80,9 @@ function config(app, { showLoginPage, serviceProvider }) {
74
80
  )
75
81
 
76
82
  try {
77
- const { profile, scopes, nonce } = lookUpByAuthCode(authCode)
83
+ const { profile, scopes, nonce } = lookUpByAuthCode(authCode, {
84
+ isStateless,
85
+ })
78
86
  console.info(
79
87
  `Profile ${JSON.stringify(profile)} with token scope ${scopes}`,
80
88
  )
@@ -120,7 +128,9 @@ function config(app, { showLoginPage, serviceProvider }) {
120
128
  req.headers.authorization || req.headers.Authorization
121
129
  ).replace('Bearer ', '')
122
130
  // eslint-disable-next-line no-unused-vars
123
- const { profile, scopes, unused } = lookUpByAuthCode(authCode)
131
+ const { profile, scopes, unused } = lookUpByAuthCode(authCode, {
132
+ isStateless,
133
+ })
124
134
  const uuid = profile.uuid
125
135
  const nric = assertions.oidc.singPass.find((p) => p.uuid === uuid).nric
126
136
  const persona = assertions.myinfo.v3.personas[nric]
@@ -258,6 +268,10 @@ const formatVehicles = (vehicles) => {
258
268
  return vehicleObjects
259
269
  }
260
270
 
271
+ const formatJsonStringify = (value) => {
272
+ return value == undefined ? 'NA' : JSON.stringify(value)
273
+ }
274
+
261
275
  const defaultUndefinedToNA = (value) => {
262
276
  return value || 'NA'
263
277
  }
@@ -310,6 +324,8 @@ const sgIDScopeToMyInfoField = (persona, scope) => {
310
324
  return defaultUndefinedToNA(persona.marital?.desc)
311
325
  case 'myinfo.mobile_number_with_country_code':
312
326
  return formatMobileNumberWithPrefix(persona.mobileno)
327
+ case 'pocdex.public_officer_details':
328
+ return formatJsonStringify(persona.publicofficerdetails)
313
329
  default:
314
330
  return 'NA'
315
331
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengovsg/mockpass",
3
- "version": "4.0.12",
3
+ "version": "4.3.2",
4
4
  "description": "A mock SingPass/CorpPass server for dev purposes",
5
5
  "main": "app.js",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "cz": "git-cz",
13
13
  "lint": "eslint lib",
14
14
  "lint-fix": "eslint --fix lib",
15
- "_postinstall": "husky install",
15
+ "prepare": "node .husky/install.mjs",
16
16
  "prepublishOnly": "pinst --disable",
17
17
  "postpublish": "pinst --enable"
18
18
  },
@@ -41,7 +41,7 @@
41
41
  "dotenv": "^16.0.0",
42
42
  "expiry-map": "^2.0.0",
43
43
  "express": "^4.16.3",
44
- "jose": "^4.14.4",
44
+ "jose": "^5.2.3",
45
45
  "jsonwebtoken": "^9.0.0",
46
46
  "lodash": "^4.17.11",
47
47
  "morgan": "^1.9.1",
@@ -50,16 +50,16 @@
50
50
  "uuid": "^9.0.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@commitlint/cli": "^17.1.2",
54
- "@commitlint/config-conventional": "^17.1.0",
55
- "@commitlint/travis-cli": "^17.1.2",
53
+ "@commitlint/cli": "^19.1.0",
54
+ "@commitlint/config-conventional": "^19.0.3",
55
+ "@commitlint/travis-cli": "^19.0.3",
56
56
  "commitizen": "^4.2.4",
57
57
  "cz-conventional-changelog": "^3.2.0",
58
58
  "eslint": "^8.0.0",
59
- "eslint-config-prettier": "^8.3.0",
59
+ "eslint-config-prettier": "^9.1.0",
60
60
  "eslint-plugin-prettier": "^4.0.0",
61
- "husky": "^8.0.1",
62
- "lint-staged": "^14.0.1",
61
+ "husky": "^9.0.11",
62
+ "lint-staged": "^15.2.2",
63
63
  "nodemon": "^3.0.1",
64
64
  "pinst": "^3.0.0",
65
65
  "prettier": "^2.0.5"
@@ -1203,7 +1203,16 @@
1203
1203
  "source": "1",
1204
1204
  "classification": "C",
1205
1205
  "desc": ""
1206
- }
1206
+ },
1207
+ "publicofficerdetails": [
1208
+ {
1209
+ "work_email": "lim_yong_xiang@was.gov.sg",
1210
+ "agency_name": "Work Allocation Singapore",
1211
+ "department_name": "Allocation Central",
1212
+ "employment_type": "Fixed Term",
1213
+ "employment_title": "Senior Software Engineer - LLv1 (Individual Contributor) (WAS)"
1214
+ }
1215
+ ]
1207
1216
  },
1208
1217
  "S9912370B": {
1209
1218
  "edulevel": {
@@ -1517,7 +1526,7 @@
1517
1526
  "code": "C",
1518
1527
  "source": "1",
1519
1528
  "classification": "C",
1520
- "desc": "Citizen"
1529
+ "desc": "CITIZEN"
1521
1530
  },
1522
1531
  "cpfbalances": {
1523
1532
  "lastupdated": "2020-04-16",
@@ -2415,7 +2424,7 @@
2415
2424
  "code": "C",
2416
2425
  "source": "1",
2417
2426
  "classification": "C",
2418
- "desc": "Citizen"
2427
+ "desc": "CITIZEN"
2419
2428
  },
2420
2429
  "cpfbalances": {
2421
2430
  "lastupdated": "2020-04-16",
@@ -7065,7 +7074,7 @@
7065
7074
  "code": "C",
7066
7075
  "source": "1",
7067
7076
  "classification": "C",
7068
- "desc": "Citizen"
7077
+ "desc": "CITIZEN"
7069
7078
  },
7070
7079
  "cpfbalances": {
7071
7080
  "lastupdated": "2020-04-16",
@@ -8214,7 +8223,7 @@
8214
8223
  "code": "C",
8215
8224
  "source": "1",
8216
8225
  "classification": "C",
8217
- "desc": "Citizen"
8226
+ "desc": "CITIZEN"
8218
8227
  },
8219
8228
  "cpfbalances": {
8220
8229
  "lastupdated": "2020-04-16",
@@ -9302,7 +9311,7 @@
9302
9311
  "code": "C",
9303
9312
  "source": "1",
9304
9313
  "classification": "C",
9305
- "desc": "Citizen"
9314
+ "desc": "CITIZEN"
9306
9315
  },
9307
9316
  "cpfbalances": {
9308
9317
  "lastupdated": "2020-04-16",
@@ -10104,7 +10113,7 @@
10104
10113
  "code": "C",
10105
10114
  "source": "1",
10106
10115
  "classification": "C",
10107
- "desc": "Citizen"
10116
+ "desc": "CITIZEN"
10108
10117
  },
10109
10118
  "cpfbalances": {
10110
10119
  "lastupdated": "2020-02-04",
@@ -12107,7 +12116,7 @@
12107
12116
  "code": "C",
12108
12117
  "source": "1",
12109
12118
  "classification": "C",
12110
- "desc": "Citizen"
12119
+ "desc": "CITIZEN"
12111
12120
  },
12112
12121
  "cpfbalances": {
12113
12122
  "lastupdated": "2020-04-16",
@@ -13817,7 +13826,7 @@
13817
13826
  "code": "C",
13818
13827
  "source": "1",
13819
13828
  "classification": "C",
13820
- "desc": "Citizen"
13829
+ "desc": "CITIZEN"
13821
13830
  },
13822
13831
  "cpfbalances": {
13823
13832
  "oa": {
@@ -14473,7 +14482,7 @@
14473
14482
  "code": "C",
14474
14483
  "source": "1",
14475
14484
  "classification": "C",
14476
- "desc": "Citizen"
14485
+ "desc": "CITIZEN"
14477
14486
  },
14478
14487
  "cpfbalances": {
14479
14488
  "lastupdated": "2020-04-16",
@@ -15150,7 +15159,7 @@
15150
15159
  "code": "C",
15151
15160
  "source": "1",
15152
15161
  "classification": "C",
15153
- "desc": "Citizen"
15162
+ "desc": "CITIZEN"
15154
15163
  },
15155
15164
  "cpfbalances": {
15156
15165
  "lastupdated": "2020-04-16",
@@ -16153,7 +16162,7 @@
16153
16162
  "code": "C",
16154
16163
  "source": "1",
16155
16164
  "classification": "C",
16156
- "desc": "Citizen"
16165
+ "desc": "CITIZEN"
16157
16166
  },
16158
16167
  "cpfbalances": {
16159
16168
  "lastupdated": "2020-04-16",
@@ -19137,7 +19146,7 @@
19137
19146
  "code": "C",
19138
19147
  "source": "1",
19139
19148
  "classification": "C",
19140
- "desc": "Citizen"
19149
+ "desc": "CITIZEN"
19141
19150
  },
19142
19151
  "cpfbalances": {
19143
19152
  "lastupdated": "2020-04-16",
@@ -24343,7 +24352,7 @@
24343
24352
  "code": "C",
24344
24353
  "source": "1",
24345
24354
  "classification": "C",
24346
- "desc": "Citizen"
24355
+ "desc": "CITIZEN"
24347
24356
  },
24348
24357
  "cpfbalances": {
24349
24358
  "lastupdated": "2020-04-16",
@@ -26126,7 +26135,7 @@
26126
26135
  "code": "C",
26127
26136
  "source": "1",
26128
26137
  "classification": "C",
26129
- "desc": "Citizen"
26138
+ "desc": "CITIZEN"
26130
26139
  },
26131
26140
  "cpfbalances": {
26132
26141
  "oa": {
@@ -26919,7 +26928,7 @@
26919
26928
  "code": "C",
26920
26929
  "source": "1",
26921
26930
  "classification": "C",
26922
- "desc": "Citizen"
26931
+ "desc": "CITIZEN"
26923
26932
  },
26924
26933
  "cpfbalances": {
26925
26934
  "lastupdated": "2020-04-16",
@@ -27960,7 +27969,7 @@
27960
27969
  "code": "C",
27961
27970
  "source": "1",
27962
27971
  "classification": "C",
27963
- "desc": "Citizen"
27972
+ "desc": "CITIZEN"
27964
27973
  },
27965
27974
  "cpfbalances": {
27966
27975
  "oa": {