@solidxai/core 0.1.15 → 0.1.16-beta.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 (171) hide show
  1. package/CHANGELOG.md +642 -0
  2. package/dist/commands/run-tests.command.d.ts +3 -1
  3. package/dist/commands/run-tests.command.d.ts.map +1 -1
  4. package/dist/commands/run-tests.command.js +6 -2
  5. package/dist/commands/run-tests.command.js.map +1 -1
  6. package/dist/controllers/test-run.controller.d.ts +12 -0
  7. package/dist/controllers/test-run.controller.d.ts.map +1 -0
  8. package/dist/controllers/test-run.controller.js +54 -0
  9. package/dist/controllers/test-run.controller.js.map +1 -0
  10. package/dist/dtos/test-run-request.dto.d.ts +35 -0
  11. package/dist/dtos/test-run-request.dto.d.ts.map +1 -0
  12. package/dist/dtos/test-run-request.dto.js +120 -0
  13. package/dist/dtos/test-run-request.dto.js.map +1 -0
  14. package/dist/guards/access-token.guard.d.ts +5 -1
  15. package/dist/guards/access-token.guard.d.ts.map +1 -1
  16. package/dist/guards/access-token.guard.js +20 -1
  17. package/dist/guards/access-token.guard.js.map +1 -1
  18. package/dist/index.d.ts +12 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +17 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/interfaces/active-user-data.interface.d.ts +1 -0
  23. package/dist/interfaces/active-user-data.interface.d.ts.map +1 -1
  24. package/dist/interfaces/active-user-data.interface.js.map +1 -1
  25. package/dist/jobs/database/test-run-queue-options-database.d.ts +8 -0
  26. package/dist/jobs/database/test-run-queue-options-database.d.ts.map +1 -0
  27. package/dist/jobs/database/test-run-queue-options-database.js +10 -0
  28. package/dist/jobs/database/test-run-queue-options-database.js.map +1 -0
  29. package/dist/jobs/database/test-run-queue-publisher-database.service.d.ts +12 -0
  30. package/dist/jobs/database/test-run-queue-publisher-database.service.d.ts.map +1 -0
  31. package/dist/jobs/database/test-run-queue-publisher-database.service.js +39 -0
  32. package/dist/jobs/database/test-run-queue-publisher-database.service.js.map +1 -0
  33. package/dist/jobs/database/test-run-queue-subscriber-database.service.d.ts +26 -0
  34. package/dist/jobs/database/test-run-queue-subscriber-database.service.d.ts.map +1 -0
  35. package/dist/jobs/database/test-run-queue-subscriber-database.service.js +120 -0
  36. package/dist/jobs/database/test-run-queue-subscriber-database.service.js.map +1 -0
  37. package/dist/seeders/seed-data/solid-core-metadata.json +3 -1
  38. package/dist/services/access-token-denylist.service.d.ts +16 -0
  39. package/dist/services/access-token-denylist.service.d.ts.map +1 -0
  40. package/dist/services/access-token-denylist.service.js +74 -0
  41. package/dist/services/access-token-denylist.service.js.map +1 -0
  42. package/dist/services/authentication.service.d.ts +9 -1
  43. package/dist/services/authentication.service.d.ts.map +1 -1
  44. package/dist/services/authentication.service.js +39 -4
  45. package/dist/services/authentication.service.js.map +1 -1
  46. package/dist/services/import-transaction.service.d.ts +1 -1
  47. package/dist/services/import-transaction.service.d.ts.map +1 -1
  48. package/dist/services/import-transaction.service.js +13 -23
  49. package/dist/services/import-transaction.service.js.map +1 -1
  50. package/dist/services/settings/default-settings-provider.service.d.ts +20 -0
  51. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  52. package/dist/services/settings/default-settings-provider.service.js +11 -0
  53. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  54. package/dist/services/workflow-secret.service.d.ts +3 -0
  55. package/dist/services/workflow-secret.service.d.ts.map +1 -1
  56. package/dist/services/workflow-secret.service.js +25 -0
  57. package/dist/services/workflow-secret.service.js.map +1 -1
  58. package/dist/solid-core.module.d.ts.map +1 -1
  59. package/dist/solid-core.module.js +9 -0
  60. package/dist/solid-core.module.js.map +1 -1
  61. package/dist/testing/adapters/ui/browser-provisioner.d.ts +5 -0
  62. package/dist/testing/adapters/ui/browser-provisioner.d.ts.map +1 -0
  63. package/dist/testing/adapters/ui/browser-provisioner.js +102 -0
  64. package/dist/testing/adapters/ui/browser-provisioner.js.map +1 -0
  65. package/dist/testing/adapters/ui/playwright-adapter.d.ts +40 -1
  66. package/dist/testing/adapters/ui/playwright-adapter.d.ts.map +1 -1
  67. package/dist/testing/adapters/ui/playwright-adapter.js +185 -3
  68. package/dist/testing/adapters/ui/playwright-adapter.js.map +1 -1
  69. package/dist/testing/adapters/ui/ui.types.d.ts +6 -0
  70. package/dist/testing/adapters/ui/ui.types.d.ts.map +1 -1
  71. package/dist/testing/adapters/ui/ui.types.js.map +1 -1
  72. package/dist/testing/contracts/runtime-context.types.d.ts +2 -0
  73. package/dist/testing/contracts/runtime-context.types.d.ts.map +1 -1
  74. package/dist/testing/contracts/runtime-context.types.js.map +1 -1
  75. package/dist/testing/core/interpolation.d.ts +1 -0
  76. package/dist/testing/core/interpolation.d.ts.map +1 -1
  77. package/dist/testing/core/interpolation.js +28 -68
  78. package/dist/testing/core/interpolation.js.map +1 -1
  79. package/dist/testing/core/path-resolution.spec.d.ts +2 -0
  80. package/dist/testing/core/path-resolution.spec.d.ts.map +1 -0
  81. package/dist/testing/core/path-resolution.spec.js +106 -0
  82. package/dist/testing/core/path-resolution.spec.js.map +1 -0
  83. package/dist/testing/core/path-segments.d.ts +2 -0
  84. package/dist/testing/core/path-segments.d.ts.map +1 -0
  85. package/dist/testing/core/path-segments.js +70 -0
  86. package/dist/testing/core/path-segments.js.map +1 -0
  87. package/dist/testing/core/resource-store.d.ts.map +1 -1
  88. package/dist/testing/core/resource-store.js +5 -2
  89. package/dist/testing/core/resource-store.js.map +1 -1
  90. package/dist/testing/core/testing-engine.d.ts.map +1 -1
  91. package/dist/testing/core/testing-engine.js +16 -0
  92. package/dist/testing/core/testing-engine.js.map +1 -1
  93. package/dist/testing/reporter/artifact-sink.d.ts +14 -0
  94. package/dist/testing/reporter/artifact-sink.d.ts.map +1 -0
  95. package/dist/testing/reporter/artifact-sink.js +3 -0
  96. package/dist/testing/reporter/artifact-sink.js.map +1 -0
  97. package/dist/testing/reporter/console-reporter.d.ts.map +1 -1
  98. package/dist/testing/reporter/console-reporter.js +7 -0
  99. package/dist/testing/reporter/console-reporter.js.map +1 -1
  100. package/dist/testing/reporter/file-service-artifact-sink.d.ts +22 -0
  101. package/dist/testing/reporter/file-service-artifact-sink.d.ts.map +1 -0
  102. package/dist/testing/reporter/file-service-artifact-sink.js +31 -0
  103. package/dist/testing/reporter/file-service-artifact-sink.js.map +1 -0
  104. package/dist/testing/reporter/lifecycle-events.types.d.ts +67 -0
  105. package/dist/testing/reporter/lifecycle-events.types.d.ts.map +1 -0
  106. package/dist/testing/reporter/lifecycle-events.types.js +3 -0
  107. package/dist/testing/reporter/lifecycle-events.types.js.map +1 -0
  108. package/dist/testing/reporter/lifecycle-webhook-reporter.d.ts +77 -0
  109. package/dist/testing/reporter/lifecycle-webhook-reporter.d.ts.map +1 -0
  110. package/dist/testing/reporter/lifecycle-webhook-reporter.js +275 -0
  111. package/dist/testing/reporter/lifecycle-webhook-reporter.js.map +1 -0
  112. package/dist/testing/reporter/progress-reporter.d.ts +3 -3
  113. package/dist/testing/reporter/progress-reporter.d.ts.map +1 -1
  114. package/dist/testing/reporter/progress-reporter.js.map +1 -1
  115. package/dist/testing/reporter/reporter.types.d.ts +13 -1
  116. package/dist/testing/reporter/reporter.types.d.ts.map +1 -1
  117. package/dist/testing/reporter/reporter.types.js.map +1 -1
  118. package/dist/testing/runner/read-scenarios-file.d.ts +9 -0
  119. package/dist/testing/runner/read-scenarios-file.d.ts.map +1 -0
  120. package/dist/testing/runner/read-scenarios-file.js +56 -0
  121. package/dist/testing/runner/read-scenarios-file.js.map +1 -0
  122. package/dist/testing/runner/run-from-metadata.d.ts +7 -2
  123. package/dist/testing/runner/run-from-metadata.d.ts.map +1 -1
  124. package/dist/testing/runner/run-from-metadata.js +57 -6
  125. package/dist/testing/runner/run-from-metadata.js.map +1 -1
  126. package/dist/testing/steps/ui/navigation.step.d.ts.map +1 -1
  127. package/dist/testing/steps/ui/navigation.step.js +12 -12
  128. package/dist/testing/steps/ui/navigation.step.js.map +1 -1
  129. package/package.json +1 -1
  130. package/postman/instant-logout.postman_collection.json +1493 -0
  131. package/src/commands/run-tests.command.ts +3 -0
  132. package/src/controllers/test-run.controller.ts +44 -0
  133. package/src/dtos/test-run-request.dto.ts +130 -0
  134. package/src/guards/access-token.guard.ts +48 -0
  135. package/src/index.ts +21 -1
  136. package/src/interfaces/active-user-data.interface.ts +8 -0
  137. package/src/jobs/database/test-run-queue-options-database.ts +9 -0
  138. package/src/jobs/database/test-run-queue-publisher-database.service.ts +24 -0
  139. package/src/jobs/database/test-run-queue-subscriber-database.service.ts +122 -0
  140. package/src/seeders/seed-data/solid-core-metadata.json +3 -1
  141. package/src/services/access-token-denylist.service.ts +145 -0
  142. package/src/services/authentication.service.ts +118 -2
  143. package/src/services/import-transaction.service.ts +19 -36
  144. package/src/services/settings/default-settings-provider.service.ts +12 -0
  145. package/src/services/workflow-secret.service.ts +61 -2
  146. package/src/solid-core.module.ts +11 -0
  147. package/src/testing/README.md +44 -1
  148. package/src/testing/adapters/ui/browser-provisioner.ts +115 -0
  149. package/src/testing/adapters/ui/playwright-adapter.ts +255 -5
  150. package/src/testing/adapters/ui/ui.types.ts +14 -0
  151. package/src/testing/contracts/runtime-context.types.ts +6 -0
  152. package/src/testing/core/interpolation.ts +39 -69
  153. package/src/testing/core/path-resolution.spec.ts +133 -0
  154. package/src/testing/core/path-segments.ts +81 -0
  155. package/src/testing/core/resource-store.ts +8 -2
  156. package/src/testing/core/testing-engine.ts +22 -0
  157. package/src/testing/reporter/artifact-sink.ts +16 -0
  158. package/src/testing/reporter/console-reporter.ts +9 -0
  159. package/src/testing/reporter/file-service-artifact-sink.ts +46 -0
  160. package/src/testing/reporter/lifecycle-events.types.ts +103 -0
  161. package/src/testing/reporter/lifecycle-webhook-reporter.ts +385 -0
  162. package/src/testing/reporter/progress-reporter.ts +3 -1
  163. package/src/testing/reporter/reporter.types.ts +10 -1
  164. package/src/testing/runner/read-scenarios-file.ts +44 -0
  165. package/src/testing/runner/run-from-metadata.ts +102 -9
  166. package/src/testing/steps/ui/navigation.step.ts +24 -20
  167. package/dist/services/access-token-storage.service.d.ts +0 -1
  168. package/dist/services/access-token-storage.service.d.ts.map +0 -1
  169. package/dist/services/access-token-storage.service.js +0 -1
  170. package/dist/services/access-token-storage.service.js.map +0 -1
  171. package/src/services/access-token-storage.service.ts +0 -0
@@ -0,0 +1,1493 @@
1
+ {
2
+ "info": {
3
+ "name": "SolidX — Instant Logout (access-token revocation)",
4
+ "description": "Verifies `invalidateAccessTokenOnLogout`.\n\n## Before you run\n\n1. Point `baseUrl` at a running app that depends on this build of @solidxai/core (include the global prefix if the app sets one, e.g. http://localhost:3000/api).\n2. Set `email` / `username` / `password` to a user that can edit settings (the settings requests need a bearer token).\n3. **Set `IAM_JWT_ACCESS_TOKEN_TTL` high (e.g. 3600) and restart the app.** Otherwise a token expiring mid-run looks exactly like a revoked one and the results are meaningless.\n4. Run folders in order with the Collection Runner. Folder A must run before B — it establishes that the default (setting off) behaviour is unchanged.\n\n## Notes\n\n- `SettingService` caches per process, so run this against a single replica. With more than one, a setting toggle only reaches the instance that served the write.\n- Without REDIS_HOST/REDIS_PORT the cache is per-process in-memory. That is fine for a single-instance test run.\n- Folder D flips `preventConcurrentLogins` on and folder E turns everything back off. If a run aborts midway, run folder E before doing anything else.",
5
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
6
+ },
7
+ "variable": [
8
+ {
9
+ "key": "baseUrl",
10
+ "value": "http://localhost:3000",
11
+ "type": "string"
12
+ },
13
+ {
14
+ "key": "email",
15
+ "value": "sa@solidxai.com",
16
+ "type": "string"
17
+ },
18
+ {
19
+ "key": "username",
20
+ "value": "sa",
21
+ "type": "string"
22
+ },
23
+ {
24
+ "key": "password",
25
+ "value": "",
26
+ "type": "string"
27
+ },
28
+ {
29
+ "key": "adminToken",
30
+ "value": "",
31
+ "type": "string"
32
+ },
33
+ {
34
+ "key": "accessToken",
35
+ "value": "",
36
+ "type": "string"
37
+ },
38
+ {
39
+ "key": "refreshToken",
40
+ "value": "",
41
+ "type": "string"
42
+ },
43
+ {
44
+ "key": "accessToken2",
45
+ "value": "",
46
+ "type": "string"
47
+ },
48
+ {
49
+ "key": "refreshToken2",
50
+ "value": "",
51
+ "type": "string"
52
+ },
53
+ {
54
+ "key": "staleAccessToken",
55
+ "value": "",
56
+ "type": "string"
57
+ },
58
+ {
59
+ "key": "staleRefreshToken",
60
+ "value": "",
61
+ "type": "string"
62
+ }
63
+ ],
64
+ "item": [
65
+ {
66
+ "name": "00 — Setup",
67
+ "item": [
68
+ {
69
+ "name": "Login as admin (for settings)",
70
+ "event": [
71
+ {
72
+ "listen": "test",
73
+ "script": {
74
+ "type": "text/javascript",
75
+ "exec": [
76
+ "pm.test('login succeeds', () => pm.response.to.have.status(200));",
77
+ "// WrapResponseInterceptor wraps every payload as",
78
+ "// { statusCode, message, error, data }. Fall back to the raw body so this",
79
+ "// also works against an app that does not register that interceptor.",
80
+ "const body = pm.response.json();",
81
+ "const b = body.data ?? body;",
82
+ "pm.collectionVariables.set('adminToken', b.accessToken);",
83
+ "pm.test('got an access token', () => pm.expect(b.accessToken).to.be.a('string').and.not.empty);"
84
+ ]
85
+ }
86
+ }
87
+ ],
88
+ "request": {
89
+ "method": "POST",
90
+ "header": [
91
+ {
92
+ "key": "Content-Type",
93
+ "value": "application/json"
94
+ }
95
+ ],
96
+ "body": {
97
+ "mode": "raw",
98
+ "raw": "{\n \"email\": \"{{email}}\",\n \"username\": \"{{username}}\",\n \"password\": \"{{password}}\"\n}"
99
+ },
100
+ "url": {
101
+ "raw": "{{baseUrl}}/iam/authenticate",
102
+ "host": [
103
+ "{{baseUrl}}"
104
+ ],
105
+ "path": [
106
+ "iam",
107
+ "authenticate"
108
+ ]
109
+ }
110
+ }
111
+ }
112
+ ]
113
+ },
114
+ {
115
+ "name": "A — Setting OFF (default): behaviour unchanged",
116
+ "description": "Proves opting out costs nothing. The access token must survive logout exactly as it does today.",
117
+ "item": [
118
+ {
119
+ "name": "A1. invalidateAccessTokenOnLogout = false",
120
+ "event": [
121
+ {
122
+ "listen": "test",
123
+ "script": {
124
+ "type": "text/javascript",
125
+ "exec": [
126
+ "pm.test('setting updated', () => pm.response.to.have.status(200));"
127
+ ]
128
+ }
129
+ }
130
+ ],
131
+ "request": {
132
+ "method": "PUT",
133
+ "header": [
134
+ {
135
+ "key": "Content-Type",
136
+ "value": "application/json"
137
+ },
138
+ {
139
+ "key": "Authorization",
140
+ "value": "Bearer {{adminToken}}"
141
+ }
142
+ ],
143
+ "body": {
144
+ "mode": "raw",
145
+ "raw": "{\n \"settings\": [\n { \"key\": \"invalidateAccessTokenOnLogout\", \"value\": \"false\", \"type\": \"system\" }\n ]\n}"
146
+ },
147
+ "url": {
148
+ "raw": "{{baseUrl}}/setting/bulk",
149
+ "host": [
150
+ "{{baseUrl}}"
151
+ ],
152
+ "path": [
153
+ "setting",
154
+ "bulk"
155
+ ]
156
+ }
157
+ }
158
+ },
159
+ {
160
+ "name": "A2. Login",
161
+ "event": [
162
+ {
163
+ "listen": "test",
164
+ "script": {
165
+ "type": "text/javascript",
166
+ "exec": [
167
+ "pm.test('login succeeds', () => pm.response.to.have.status(200));",
168
+ "// WrapResponseInterceptor wraps every payload as",
169
+ "// { statusCode, message, error, data }. Fall back to the raw body so this",
170
+ "// also works against an app that does not register that interceptor.",
171
+ "const body = pm.response.json();",
172
+ "const b = body.data ?? body;",
173
+ "pm.collectionVariables.set('accessToken', b.accessToken);",
174
+ "pm.collectionVariables.set('refreshToken', b.refreshToken);"
175
+ ]
176
+ }
177
+ }
178
+ ],
179
+ "request": {
180
+ "method": "POST",
181
+ "header": [
182
+ {
183
+ "key": "Content-Type",
184
+ "value": "application/json"
185
+ }
186
+ ],
187
+ "body": {
188
+ "mode": "raw",
189
+ "raw": "{\n \"email\": \"{{email}}\",\n \"username\": \"{{username}}\",\n \"password\": \"{{password}}\"\n}"
190
+ },
191
+ "url": {
192
+ "raw": "{{baseUrl}}/iam/authenticate",
193
+ "host": [
194
+ "{{baseUrl}}"
195
+ ],
196
+ "path": [
197
+ "iam",
198
+ "authenticate"
199
+ ]
200
+ }
201
+ }
202
+ },
203
+ {
204
+ "name": "A3. GET /iam/me → 200",
205
+ "event": [
206
+ {
207
+ "listen": "test",
208
+ "script": {
209
+ "type": "text/javascript",
210
+ "exec": [
211
+ "pm.test('token works before logout', () => pm.response.to.have.status(200));"
212
+ ]
213
+ }
214
+ }
215
+ ],
216
+ "request": {
217
+ "method": "GET",
218
+ "header": [
219
+ {
220
+ "key": "Authorization",
221
+ "value": "Bearer {{accessToken}}"
222
+ }
223
+ ],
224
+ "url": {
225
+ "raw": "{{baseUrl}}/iam/me",
226
+ "host": [
227
+ "{{baseUrl}}"
228
+ ],
229
+ "path": [
230
+ "iam",
231
+ "me"
232
+ ]
233
+ }
234
+ }
235
+ },
236
+ {
237
+ "name": "A4. Logout",
238
+ "event": [
239
+ {
240
+ "listen": "test",
241
+ "script": {
242
+ "type": "text/javascript",
243
+ "exec": [
244
+ "pm.test('logout returns 200', () => pm.response.to.have.status(200));"
245
+ ]
246
+ }
247
+ }
248
+ ],
249
+ "request": {
250
+ "method": "POST",
251
+ "header": [
252
+ {
253
+ "key": "Content-Type",
254
+ "value": "application/json"
255
+ }
256
+ ],
257
+ "body": {
258
+ "mode": "raw",
259
+ "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}"
260
+ },
261
+ "url": {
262
+ "raw": "{{baseUrl}}/iam/logout",
263
+ "host": [
264
+ "{{baseUrl}}"
265
+ ],
266
+ "path": [
267
+ "iam",
268
+ "logout"
269
+ ]
270
+ }
271
+ }
272
+ },
273
+ {
274
+ "name": "A5. GET /iam/me with the SAME token → still 200",
275
+ "event": [
276
+ {
277
+ "listen": "test",
278
+ "script": {
279
+ "type": "text/javascript",
280
+ "exec": [
281
+ "// With the feature off, logout must NOT invalidate the access token.",
282
+ "// A 401 here means the setting is being ignored and opted-out",
283
+ "// deployments would see a behaviour change.",
284
+ "pm.test('access token survives logout when feature is off', () => pm.response.to.have.status(200));"
285
+ ]
286
+ }
287
+ }
288
+ ],
289
+ "request": {
290
+ "method": "GET",
291
+ "header": [
292
+ {
293
+ "key": "Authorization",
294
+ "value": "Bearer {{accessToken}}"
295
+ }
296
+ ],
297
+ "url": {
298
+ "raw": "{{baseUrl}}/iam/me",
299
+ "host": [
300
+ "{{baseUrl}}"
301
+ ],
302
+ "path": [
303
+ "iam",
304
+ "me"
305
+ ]
306
+ }
307
+ }
308
+ }
309
+ ]
310
+ },
311
+ {
312
+ "name": "B — Setting ON: instant logout",
313
+ "description": "The feature itself. Step B5 is the fix.",
314
+ "item": [
315
+ {
316
+ "name": "B1. invalidateAccessTokenOnLogout = true",
317
+ "event": [
318
+ {
319
+ "listen": "test",
320
+ "script": {
321
+ "type": "text/javascript",
322
+ "exec": [
323
+ "pm.test('setting updated', () => pm.response.to.have.status(200));"
324
+ ]
325
+ }
326
+ }
327
+ ],
328
+ "request": {
329
+ "method": "PUT",
330
+ "header": [
331
+ {
332
+ "key": "Content-Type",
333
+ "value": "application/json"
334
+ },
335
+ {
336
+ "key": "Authorization",
337
+ "value": "Bearer {{adminToken}}"
338
+ }
339
+ ],
340
+ "body": {
341
+ "mode": "raw",
342
+ "raw": "{\n \"settings\": [\n { \"key\": \"invalidateAccessTokenOnLogout\", \"value\": \"true\", \"type\": \"system\" }\n ]\n}"
343
+ },
344
+ "url": {
345
+ "raw": "{{baseUrl}}/setting/bulk",
346
+ "host": [
347
+ "{{baseUrl}}"
348
+ ],
349
+ "path": [
350
+ "setting",
351
+ "bulk"
352
+ ]
353
+ }
354
+ }
355
+ },
356
+ {
357
+ "name": "B2. Login",
358
+ "event": [
359
+ {
360
+ "listen": "test",
361
+ "script": {
362
+ "type": "text/javascript",
363
+ "exec": [
364
+ "pm.test('login succeeds', () => pm.response.to.have.status(200));",
365
+ "// WrapResponseInterceptor wraps every payload as",
366
+ "// { statusCode, message, error, data }. Fall back to the raw body so this",
367
+ "// also works against an app that does not register that interceptor.",
368
+ "const body = pm.response.json();",
369
+ "const b = body.data ?? body;",
370
+ "pm.collectionVariables.set('accessToken', b.accessToken);",
371
+ "pm.collectionVariables.set('refreshToken', b.refreshToken);"
372
+ ]
373
+ }
374
+ }
375
+ ],
376
+ "request": {
377
+ "method": "POST",
378
+ "header": [
379
+ {
380
+ "key": "Content-Type",
381
+ "value": "application/json"
382
+ }
383
+ ],
384
+ "body": {
385
+ "mode": "raw",
386
+ "raw": "{\n \"email\": \"{{email}}\",\n \"username\": \"{{username}}\",\n \"password\": \"{{password}}\"\n}"
387
+ },
388
+ "url": {
389
+ "raw": "{{baseUrl}}/iam/authenticate",
390
+ "host": [
391
+ "{{baseUrl}}"
392
+ ],
393
+ "path": [
394
+ "iam",
395
+ "authenticate"
396
+ ]
397
+ }
398
+ }
399
+ },
400
+ {
401
+ "name": "B3. GET /iam/me → 200",
402
+ "event": [
403
+ {
404
+ "listen": "test",
405
+ "script": {
406
+ "type": "text/javascript",
407
+ "exec": [
408
+ "pm.test('token works before logout', () => pm.response.to.have.status(200));"
409
+ ]
410
+ }
411
+ }
412
+ ],
413
+ "request": {
414
+ "method": "GET",
415
+ "header": [
416
+ {
417
+ "key": "Authorization",
418
+ "value": "Bearer {{accessToken}}"
419
+ }
420
+ ],
421
+ "url": {
422
+ "raw": "{{baseUrl}}/iam/me",
423
+ "host": [
424
+ "{{baseUrl}}"
425
+ ],
426
+ "path": [
427
+ "iam",
428
+ "me"
429
+ ]
430
+ }
431
+ }
432
+ },
433
+ {
434
+ "name": "B4. Logout",
435
+ "event": [
436
+ {
437
+ "listen": "test",
438
+ "script": {
439
+ "type": "text/javascript",
440
+ "exec": [
441
+ "pm.test('logout returns 200', () => pm.response.to.have.status(200));"
442
+ ]
443
+ }
444
+ }
445
+ ],
446
+ "request": {
447
+ "method": "POST",
448
+ "header": [
449
+ {
450
+ "key": "Content-Type",
451
+ "value": "application/json"
452
+ }
453
+ ],
454
+ "body": {
455
+ "mode": "raw",
456
+ "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}"
457
+ },
458
+ "url": {
459
+ "raw": "{{baseUrl}}/iam/logout",
460
+ "host": [
461
+ "{{baseUrl}}"
462
+ ],
463
+ "path": [
464
+ "iam",
465
+ "logout"
466
+ ]
467
+ }
468
+ }
469
+ },
470
+ {
471
+ "name": "B5. GET /iam/me with the SAME token → 401 ← THE FIX",
472
+ "event": [
473
+ {
474
+ "listen": "test",
475
+ "script": {
476
+ "type": "text/javascript",
477
+ "exec": [
478
+ "// The whole point. Before this change the token stayed valid for the",
479
+ "// remainder of accessTokenTtl (20 min in prod).",
480
+ "pm.test('access token is revoked immediately', () => pm.response.to.have.status(401));"
481
+ ]
482
+ }
483
+ }
484
+ ],
485
+ "request": {
486
+ "method": "GET",
487
+ "header": [
488
+ {
489
+ "key": "Authorization",
490
+ "value": "Bearer {{accessToken}}"
491
+ }
492
+ ],
493
+ "url": {
494
+ "raw": "{{baseUrl}}/iam/me",
495
+ "host": [
496
+ "{{baseUrl}}"
497
+ ],
498
+ "path": [
499
+ "iam",
500
+ "me"
501
+ ]
502
+ }
503
+ }
504
+ },
505
+ {
506
+ "name": "B6. Refresh with the old refresh token → 401",
507
+ "event": [
508
+ {
509
+ "listen": "test",
510
+ "script": {
511
+ "type": "text/javascript",
512
+ "exec": [
513
+ "pm.test('refresh token also invalidated (unchanged behaviour)', () => pm.response.to.have.status(401));"
514
+ ]
515
+ }
516
+ }
517
+ ],
518
+ "request": {
519
+ "method": "POST",
520
+ "header": [
521
+ {
522
+ "key": "Content-Type",
523
+ "value": "application/json"
524
+ }
525
+ ],
526
+ "body": {
527
+ "mode": "raw",
528
+ "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}"
529
+ },
530
+ "url": {
531
+ "raw": "{{baseUrl}}/iam/refresh-tokens",
532
+ "host": [
533
+ "{{baseUrl}}"
534
+ ],
535
+ "path": [
536
+ "iam",
537
+ "refresh-tokens"
538
+ ]
539
+ }
540
+ }
541
+ },
542
+ {
543
+ "name": "B7. Log in again → new token works",
544
+ "event": [
545
+ {
546
+ "listen": "test",
547
+ "script": {
548
+ "type": "text/javascript",
549
+ "exec": [
550
+ "pm.test('login still works after logout', () => pm.response.to.have.status(200));",
551
+ "// WrapResponseInterceptor wraps every payload as",
552
+ "// { statusCode, message, error, data }. Fall back to the raw body so this",
553
+ "// also works against an app that does not register that interceptor.",
554
+ "const body = pm.response.json();",
555
+ "const b = body.data ?? body;",
556
+ "pm.collectionVariables.set('accessToken', b.accessToken);",
557
+ "pm.collectionVariables.set('refreshToken', b.refreshToken);"
558
+ ]
559
+ }
560
+ }
561
+ ],
562
+ "request": {
563
+ "method": "POST",
564
+ "header": [
565
+ {
566
+ "key": "Content-Type",
567
+ "value": "application/json"
568
+ }
569
+ ],
570
+ "body": {
571
+ "mode": "raw",
572
+ "raw": "{\n \"email\": \"{{email}}\",\n \"username\": \"{{username}}\",\n \"password\": \"{{password}}\"\n}"
573
+ },
574
+ "url": {
575
+ "raw": "{{baseUrl}}/iam/authenticate",
576
+ "host": [
577
+ "{{baseUrl}}"
578
+ ],
579
+ "path": [
580
+ "iam",
581
+ "authenticate"
582
+ ]
583
+ }
584
+ }
585
+ },
586
+ {
587
+ "name": "B8. GET /iam/me with the NEW token → 200 (user not poisoned)",
588
+ "event": [
589
+ {
590
+ "listen": "test",
591
+ "script": {
592
+ "type": "text/javascript",
593
+ "exec": [
594
+ "// Guards against a revocation entry outliving its session and killing",
595
+ "// freshly minted tokens - the failure a boolean denylist would have.",
596
+ "pm.test('new token is not born revoked', () => pm.response.to.have.status(200));"
597
+ ]
598
+ }
599
+ }
600
+ ],
601
+ "request": {
602
+ "method": "GET",
603
+ "header": [
604
+ {
605
+ "key": "Authorization",
606
+ "value": "Bearer {{accessToken}}"
607
+ }
608
+ ],
609
+ "url": {
610
+ "raw": "{{baseUrl}}/iam/me",
611
+ "host": [
612
+ "{{baseUrl}}"
613
+ ],
614
+ "path": [
615
+ "iam",
616
+ "me"
617
+ ]
618
+ }
619
+ }
620
+ }
621
+ ]
622
+ },
623
+ {
624
+ "name": "C — Scope: one session vs allDevices",
625
+ "description": "Requires preventConcurrentLogins = false (the default) so two sessions can coexist.",
626
+ "item": [
627
+ {
628
+ "name": "C1. Login as client 1",
629
+ "event": [
630
+ {
631
+ "listen": "test",
632
+ "script": {
633
+ "type": "text/javascript",
634
+ "exec": [
635
+ "pm.test('client 1 logged in', () => pm.response.to.have.status(200));",
636
+ "// WrapResponseInterceptor wraps every payload as",
637
+ "// { statusCode, message, error, data }. Fall back to the raw body so this",
638
+ "// also works against an app that does not register that interceptor.",
639
+ "const body = pm.response.json();",
640
+ "const b = body.data ?? body;",
641
+ "pm.collectionVariables.set('accessToken', b.accessToken);",
642
+ "pm.collectionVariables.set('refreshToken', b.refreshToken);"
643
+ ]
644
+ }
645
+ }
646
+ ],
647
+ "request": {
648
+ "method": "POST",
649
+ "header": [
650
+ {
651
+ "key": "Content-Type",
652
+ "value": "application/json"
653
+ }
654
+ ],
655
+ "body": {
656
+ "mode": "raw",
657
+ "raw": "{\n \"email\": \"{{email}}\",\n \"username\": \"{{username}}\",\n \"password\": \"{{password}}\"\n}"
658
+ },
659
+ "url": {
660
+ "raw": "{{baseUrl}}/iam/authenticate",
661
+ "host": [
662
+ "{{baseUrl}}"
663
+ ],
664
+ "path": [
665
+ "iam",
666
+ "authenticate"
667
+ ]
668
+ }
669
+ }
670
+ },
671
+ {
672
+ "name": "C2. Login as client 2",
673
+ "event": [
674
+ {
675
+ "listen": "test",
676
+ "script": {
677
+ "type": "text/javascript",
678
+ "exec": [
679
+ "pm.test('client 2 logged in', () => pm.response.to.have.status(200));",
680
+ "// WrapResponseInterceptor wraps every payload as",
681
+ "// { statusCode, message, error, data }. Fall back to the raw body so this",
682
+ "// also works against an app that does not register that interceptor.",
683
+ "const body = pm.response.json();",
684
+ "const b = body.data ?? body;",
685
+ "pm.collectionVariables.set('accessToken2', b.accessToken);",
686
+ "pm.collectionVariables.set('refreshToken2', b.refreshToken);"
687
+ ]
688
+ }
689
+ }
690
+ ],
691
+ "request": {
692
+ "method": "POST",
693
+ "header": [
694
+ {
695
+ "key": "Content-Type",
696
+ "value": "application/json"
697
+ }
698
+ ],
699
+ "body": {
700
+ "mode": "raw",
701
+ "raw": "{\n \"email\": \"{{email}}\",\n \"username\": \"{{username}}\",\n \"password\": \"{{password}}\"\n}"
702
+ },
703
+ "url": {
704
+ "raw": "{{baseUrl}}/iam/authenticate",
705
+ "host": [
706
+ "{{baseUrl}}"
707
+ ],
708
+ "path": [
709
+ "iam",
710
+ "authenticate"
711
+ ]
712
+ }
713
+ }
714
+ },
715
+ {
716
+ "name": "C3. Logout client 1 (scoped)",
717
+ "event": [
718
+ {
719
+ "listen": "test",
720
+ "script": {
721
+ "type": "text/javascript",
722
+ "exec": [
723
+ "pm.test('logout returns 200', () => pm.response.to.have.status(200));"
724
+ ]
725
+ }
726
+ }
727
+ ],
728
+ "request": {
729
+ "method": "POST",
730
+ "header": [
731
+ {
732
+ "key": "Content-Type",
733
+ "value": "application/json"
734
+ }
735
+ ],
736
+ "body": {
737
+ "mode": "raw",
738
+ "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}"
739
+ },
740
+ "url": {
741
+ "raw": "{{baseUrl}}/iam/logout",
742
+ "host": [
743
+ "{{baseUrl}}"
744
+ ],
745
+ "path": [
746
+ "iam",
747
+ "logout"
748
+ ]
749
+ }
750
+ }
751
+ },
752
+ {
753
+ "name": "C4. Client 1 token → 401",
754
+ "event": [
755
+ {
756
+ "listen": "test",
757
+ "script": {
758
+ "type": "text/javascript",
759
+ "exec": [
760
+ "pm.test('client 1 is revoked', () => pm.response.to.have.status(401));"
761
+ ]
762
+ }
763
+ }
764
+ ],
765
+ "request": {
766
+ "method": "GET",
767
+ "header": [
768
+ {
769
+ "key": "Authorization",
770
+ "value": "Bearer {{accessToken}}"
771
+ }
772
+ ],
773
+ "url": {
774
+ "raw": "{{baseUrl}}/iam/me",
775
+ "host": [
776
+ "{{baseUrl}}"
777
+ ],
778
+ "path": [
779
+ "iam",
780
+ "me"
781
+ ]
782
+ }
783
+ }
784
+ },
785
+ {
786
+ "name": "C5. Client 2 token → 200 (scoping works)",
787
+ "event": [
788
+ {
789
+ "listen": "test",
790
+ "script": {
791
+ "type": "text/javascript",
792
+ "exec": [
793
+ "// A scoped logout must not touch other devices. A 401 here means the",
794
+ "// per-session key is wrong and logout is acting user-wide.",
795
+ "pm.test('client 2 is untouched', () => pm.response.to.have.status(200));"
796
+ ]
797
+ }
798
+ }
799
+ ],
800
+ "request": {
801
+ "method": "GET",
802
+ "header": [
803
+ {
804
+ "key": "Authorization",
805
+ "value": "Bearer {{accessToken2}}"
806
+ }
807
+ ],
808
+ "url": {
809
+ "raw": "{{baseUrl}}/iam/me",
810
+ "host": [
811
+ "{{baseUrl}}"
812
+ ],
813
+ "path": [
814
+ "iam",
815
+ "me"
816
+ ]
817
+ }
818
+ }
819
+ },
820
+ {
821
+ "name": "C6. Login client 1 again",
822
+ "event": [
823
+ {
824
+ "listen": "test",
825
+ "script": {
826
+ "type": "text/javascript",
827
+ "exec": [
828
+ "pm.test('client 1 logged back in', () => pm.response.to.have.status(200));",
829
+ "// WrapResponseInterceptor wraps every payload as",
830
+ "// { statusCode, message, error, data }. Fall back to the raw body so this",
831
+ "// also works against an app that does not register that interceptor.",
832
+ "const body = pm.response.json();",
833
+ "const b = body.data ?? body;",
834
+ "pm.collectionVariables.set('accessToken', b.accessToken);",
835
+ "pm.collectionVariables.set('refreshToken', b.refreshToken);"
836
+ ]
837
+ }
838
+ }
839
+ ],
840
+ "request": {
841
+ "method": "POST",
842
+ "header": [
843
+ {
844
+ "key": "Content-Type",
845
+ "value": "application/json"
846
+ }
847
+ ],
848
+ "body": {
849
+ "mode": "raw",
850
+ "raw": "{\n \"email\": \"{{email}}\",\n \"username\": \"{{username}}\",\n \"password\": \"{{password}}\"\n}"
851
+ },
852
+ "url": {
853
+ "raw": "{{baseUrl}}/iam/authenticate",
854
+ "host": [
855
+ "{{baseUrl}}"
856
+ ],
857
+ "path": [
858
+ "iam",
859
+ "authenticate"
860
+ ]
861
+ }
862
+ }
863
+ },
864
+ {
865
+ "name": "C7. Logout with allDevices: true",
866
+ "event": [
867
+ {
868
+ "listen": "test",
869
+ "script": {
870
+ "type": "text/javascript",
871
+ "exec": [
872
+ "pm.test('logout returns 200', () => pm.response.to.have.status(200));"
873
+ ]
874
+ }
875
+ }
876
+ ],
877
+ "request": {
878
+ "method": "POST",
879
+ "header": [
880
+ {
881
+ "key": "Content-Type",
882
+ "value": "application/json"
883
+ }
884
+ ],
885
+ "body": {
886
+ "mode": "raw",
887
+ "raw": "{\n \"refreshToken\": \"{{refreshToken}}\",\n \"allDevices\": true\n}"
888
+ },
889
+ "url": {
890
+ "raw": "{{baseUrl}}/iam/logout",
891
+ "host": [
892
+ "{{baseUrl}}"
893
+ ],
894
+ "path": [
895
+ "iam",
896
+ "logout"
897
+ ]
898
+ }
899
+ }
900
+ },
901
+ {
902
+ "name": "C8. Client 1 token → 401",
903
+ "event": [
904
+ {
905
+ "listen": "test",
906
+ "script": {
907
+ "type": "text/javascript",
908
+ "exec": [
909
+ "pm.test('client 1 revoked', () => pm.response.to.have.status(401));"
910
+ ]
911
+ }
912
+ }
913
+ ],
914
+ "request": {
915
+ "method": "GET",
916
+ "header": [
917
+ {
918
+ "key": "Authorization",
919
+ "value": "Bearer {{accessToken}}"
920
+ }
921
+ ],
922
+ "url": {
923
+ "raw": "{{baseUrl}}/iam/me",
924
+ "host": [
925
+ "{{baseUrl}}"
926
+ ],
927
+ "path": [
928
+ "iam",
929
+ "me"
930
+ ]
931
+ }
932
+ }
933
+ },
934
+ {
935
+ "name": "C9. Client 2 token → 401 (allDevices swept it)",
936
+ "event": [
937
+ {
938
+ "listen": "test",
939
+ "script": {
940
+ "type": "text/javascript",
941
+ "exec": [
942
+ "// The per-user revocation key. Client 2 never presented a token to",
943
+ "// this logout, yet must still be revoked.",
944
+ "pm.test('allDevices revokes other sessions too', () => pm.response.to.have.status(401));"
945
+ ]
946
+ }
947
+ }
948
+ ],
949
+ "request": {
950
+ "method": "GET",
951
+ "header": [
952
+ {
953
+ "key": "Authorization",
954
+ "value": "Bearer {{accessToken2}}"
955
+ }
956
+ ],
957
+ "url": {
958
+ "raw": "{{baseUrl}}/iam/me",
959
+ "host": [
960
+ "{{baseUrl}}"
961
+ ],
962
+ "path": [
963
+ "iam",
964
+ "me"
965
+ ]
966
+ }
967
+ }
968
+ },
969
+ {
970
+ "name": "C10. Login again → works (per-user entry is a timestamp, not a flag)",
971
+ "event": [
972
+ {
973
+ "listen": "test",
974
+ "script": {
975
+ "type": "text/javascript",
976
+ "exec": [
977
+ "pm.test('login after allDevices logout succeeds', () => pm.response.to.have.status(200));",
978
+ "// WrapResponseInterceptor wraps every payload as",
979
+ "// { statusCode, message, error, data }. Fall back to the raw body so this",
980
+ "// also works against an app that does not register that interceptor.",
981
+ "const body = pm.response.json();",
982
+ "const b = body.data ?? body;",
983
+ "pm.collectionVariables.set('accessToken', b.accessToken);",
984
+ "pm.collectionVariables.set('refreshToken', b.refreshToken);"
985
+ ]
986
+ }
987
+ }
988
+ ],
989
+ "request": {
990
+ "method": "POST",
991
+ "header": [
992
+ {
993
+ "key": "Content-Type",
994
+ "value": "application/json"
995
+ }
996
+ ],
997
+ "body": {
998
+ "mode": "raw",
999
+ "raw": "{\n \"email\": \"{{email}}\",\n \"username\": \"{{username}}\",\n \"password\": \"{{password}}\"\n}"
1000
+ },
1001
+ "url": {
1002
+ "raw": "{{baseUrl}}/iam/authenticate",
1003
+ "host": [
1004
+ "{{baseUrl}}"
1005
+ ],
1006
+ "path": [
1007
+ "iam",
1008
+ "authenticate"
1009
+ ]
1010
+ }
1011
+ }
1012
+ },
1013
+ {
1014
+ "name": "C11. New token → 200 ← boolean flag would fail here",
1015
+ "event": [
1016
+ {
1017
+ "listen": "test",
1018
+ "script": {
1019
+ "type": "text/javascript",
1020
+ "exec": [
1021
+ "// revoked-user-<id> is shared by every login this user will ever make.",
1022
+ "// A boolean entry would lock the account out until the entry expired;",
1023
+ "// the timestamp lets a newer token through.",
1024
+ "pm.test('user is not locked out by allDevices logout', () => pm.response.to.have.status(200));"
1025
+ ]
1026
+ }
1027
+ }
1028
+ ],
1029
+ "request": {
1030
+ "method": "GET",
1031
+ "header": [
1032
+ {
1033
+ "key": "Authorization",
1034
+ "value": "Bearer {{accessToken}}"
1035
+ }
1036
+ ],
1037
+ "url": {
1038
+ "raw": "{{baseUrl}}/iam/me",
1039
+ "host": [
1040
+ "{{baseUrl}}"
1041
+ ],
1042
+ "path": [
1043
+ "iam",
1044
+ "me"
1045
+ ]
1046
+ }
1047
+ }
1048
+ }
1049
+ ]
1050
+ },
1051
+ {
1052
+ "name": "D — Stale logout must not kill the live session",
1053
+ "description": "The ownership check. Needs preventConcurrentLogins = true, where all sessions share the bare `user-<id>` bucket. Folder E turns it back off.",
1054
+ "item": [
1055
+ {
1056
+ "name": "D1. preventConcurrentLogins = true",
1057
+ "event": [
1058
+ {
1059
+ "listen": "test",
1060
+ "script": {
1061
+ "type": "text/javascript",
1062
+ "exec": [
1063
+ "pm.test('setting updated', () => pm.response.to.have.status(200));"
1064
+ ]
1065
+ }
1066
+ }
1067
+ ],
1068
+ "request": {
1069
+ "method": "PUT",
1070
+ "header": [
1071
+ {
1072
+ "key": "Content-Type",
1073
+ "value": "application/json"
1074
+ },
1075
+ {
1076
+ "key": "Authorization",
1077
+ "value": "Bearer {{adminToken}}"
1078
+ }
1079
+ ],
1080
+ "body": {
1081
+ "mode": "raw",
1082
+ "raw": "{\n \"settings\": [\n { \"key\": \"preventConcurrentLogins\", \"value\": \"true\", \"type\": \"system\" },\n { \"key\": \"invalidateAccessTokenOnLogout\", \"value\": \"true\", \"type\": \"system\" }\n ]\n}"
1083
+ },
1084
+ "url": {
1085
+ "raw": "{{baseUrl}}/setting/bulk",
1086
+ "host": [
1087
+ "{{baseUrl}}"
1088
+ ],
1089
+ "path": [
1090
+ "setting",
1091
+ "bulk"
1092
+ ]
1093
+ }
1094
+ }
1095
+ },
1096
+ {
1097
+ "name": "D2. Login as client 1 (will be superseded)",
1098
+ "event": [
1099
+ {
1100
+ "listen": "test",
1101
+ "script": {
1102
+ "type": "text/javascript",
1103
+ "exec": [
1104
+ "pm.test('client 1 logged in', () => pm.response.to.have.status(200));",
1105
+ "// WrapResponseInterceptor wraps every payload as",
1106
+ "// { statusCode, message, error, data }. Fall back to the raw body so this",
1107
+ "// also works against an app that does not register that interceptor.",
1108
+ "const body = pm.response.json();",
1109
+ "const b = body.data ?? body;",
1110
+ "pm.collectionVariables.set('staleAccessToken', b.accessToken);",
1111
+ "pm.collectionVariables.set('staleRefreshToken', b.refreshToken);"
1112
+ ]
1113
+ }
1114
+ }
1115
+ ],
1116
+ "request": {
1117
+ "method": "POST",
1118
+ "header": [
1119
+ {
1120
+ "key": "Content-Type",
1121
+ "value": "application/json"
1122
+ }
1123
+ ],
1124
+ "body": {
1125
+ "mode": "raw",
1126
+ "raw": "{\n \"email\": \"{{email}}\",\n \"username\": \"{{username}}\",\n \"password\": \"{{password}}\"\n}"
1127
+ },
1128
+ "url": {
1129
+ "raw": "{{baseUrl}}/iam/authenticate",
1130
+ "host": [
1131
+ "{{baseUrl}}"
1132
+ ],
1133
+ "path": [
1134
+ "iam",
1135
+ "authenticate"
1136
+ ]
1137
+ }
1138
+ }
1139
+ },
1140
+ {
1141
+ "name": "D3. Login as client 2 (supersedes client 1)",
1142
+ "event": [
1143
+ {
1144
+ "listen": "test",
1145
+ "script": {
1146
+ "type": "text/javascript",
1147
+ "exec": [
1148
+ "pm.test('client 2 logged in', () => pm.response.to.have.status(200));",
1149
+ "// WrapResponseInterceptor wraps every payload as",
1150
+ "// { statusCode, message, error, data }. Fall back to the raw body so this",
1151
+ "// also works against an app that does not register that interceptor.",
1152
+ "const body = pm.response.json();",
1153
+ "const b = body.data ?? body;",
1154
+ "pm.collectionVariables.set('accessToken2', b.accessToken);",
1155
+ "pm.collectionVariables.set('refreshToken2', b.refreshToken);"
1156
+ ]
1157
+ }
1158
+ }
1159
+ ],
1160
+ "request": {
1161
+ "method": "POST",
1162
+ "header": [
1163
+ {
1164
+ "key": "Content-Type",
1165
+ "value": "application/json"
1166
+ }
1167
+ ],
1168
+ "body": {
1169
+ "mode": "raw",
1170
+ "raw": "{\n \"email\": \"{{email}}\",\n \"username\": \"{{username}}\",\n \"password\": \"{{password}}\"\n}"
1171
+ },
1172
+ "url": {
1173
+ "raw": "{{baseUrl}}/iam/authenticate",
1174
+ "host": [
1175
+ "{{baseUrl}}"
1176
+ ],
1177
+ "path": [
1178
+ "iam",
1179
+ "authenticate"
1180
+ ]
1181
+ }
1182
+ }
1183
+ },
1184
+ {
1185
+ "name": "D4. Client 1 token → 401 (concurrent-login check working)",
1186
+ "event": [
1187
+ {
1188
+ "listen": "test",
1189
+ "script": {
1190
+ "type": "text/javascript",
1191
+ "exec": [
1192
+ "pm.test('superseded session is rejected', () => pm.response.to.have.status(401));"
1193
+ ]
1194
+ }
1195
+ }
1196
+ ],
1197
+ "request": {
1198
+ "method": "GET",
1199
+ "header": [
1200
+ {
1201
+ "key": "Authorization",
1202
+ "value": "Bearer {{staleAccessToken}}"
1203
+ }
1204
+ ],
1205
+ "url": {
1206
+ "raw": "{{baseUrl}}/iam/me",
1207
+ "host": [
1208
+ "{{baseUrl}}"
1209
+ ],
1210
+ "path": [
1211
+ "iam",
1212
+ "me"
1213
+ ]
1214
+ }
1215
+ }
1216
+ },
1217
+ {
1218
+ "name": "D5. Client 2 token → 200",
1219
+ "event": [
1220
+ {
1221
+ "listen": "test",
1222
+ "script": {
1223
+ "type": "text/javascript",
1224
+ "exec": [
1225
+ "pm.test('live session works', () => pm.response.to.have.status(200));"
1226
+ ]
1227
+ }
1228
+ }
1229
+ ],
1230
+ "request": {
1231
+ "method": "GET",
1232
+ "header": [
1233
+ {
1234
+ "key": "Authorization",
1235
+ "value": "Bearer {{accessToken2}}"
1236
+ }
1237
+ ],
1238
+ "url": {
1239
+ "raw": "{{baseUrl}}/iam/me",
1240
+ "host": [
1241
+ "{{baseUrl}}"
1242
+ ],
1243
+ "path": [
1244
+ "iam",
1245
+ "me"
1246
+ ]
1247
+ }
1248
+ }
1249
+ },
1250
+ {
1251
+ "name": "D6. Logout with the STALE refresh token",
1252
+ "event": [
1253
+ {
1254
+ "listen": "test",
1255
+ "script": {
1256
+ "type": "text/javascript",
1257
+ "exec": [
1258
+ "// Client 1 tidying up after its 401. Still reports success (RFC 7009).",
1259
+ "pm.test('stale logout returns 200', () => pm.response.to.have.status(200));",
1260
+ "// The server logs: 'refresh token does not match the live session'"
1261
+ ]
1262
+ }
1263
+ }
1264
+ ],
1265
+ "request": {
1266
+ "method": "POST",
1267
+ "header": [
1268
+ {
1269
+ "key": "Content-Type",
1270
+ "value": "application/json"
1271
+ }
1272
+ ],
1273
+ "body": {
1274
+ "mode": "raw",
1275
+ "raw": "{\n \"refreshToken\": \"{{staleRefreshToken}}\"\n}"
1276
+ },
1277
+ "url": {
1278
+ "raw": "{{baseUrl}}/iam/logout",
1279
+ "host": [
1280
+ "{{baseUrl}}"
1281
+ ],
1282
+ "path": [
1283
+ "iam",
1284
+ "logout"
1285
+ ]
1286
+ }
1287
+ }
1288
+ },
1289
+ {
1290
+ "name": "D7. Client 2 access token → still 200 ← THE OWNERSHIP FIX",
1291
+ "event": [
1292
+ {
1293
+ "listen": "test",
1294
+ "script": {
1295
+ "type": "text/javascript",
1296
+ "exec": [
1297
+ "// Without ownsLiveSession this is 401: the stale logout resolves the",
1298
+ "// revocation key via getActiveSession, which now holds client 2's",
1299
+ "// session. The laptop's cleanup would log out the phone.",
1300
+ "pm.test('stale logout did not revoke the live session', () => pm.response.to.have.status(200));"
1301
+ ]
1302
+ }
1303
+ }
1304
+ ],
1305
+ "request": {
1306
+ "method": "GET",
1307
+ "header": [
1308
+ {
1309
+ "key": "Authorization",
1310
+ "value": "Bearer {{accessToken2}}"
1311
+ }
1312
+ ],
1313
+ "url": {
1314
+ "raw": "{{baseUrl}}/iam/me",
1315
+ "host": [
1316
+ "{{baseUrl}}"
1317
+ ],
1318
+ "path": [
1319
+ "iam",
1320
+ "me"
1321
+ ]
1322
+ }
1323
+ }
1324
+ },
1325
+ {
1326
+ "name": "D8. Client 2 refresh token → still works",
1327
+ "event": [
1328
+ {
1329
+ "listen": "test",
1330
+ "script": {
1331
+ "type": "text/javascript",
1332
+ "exec": [
1333
+ "// The pre-existing half: invalidate() would otherwise have deleted the",
1334
+ "// bare user-<id> bucket, which by now holds client 2's refresh token.",
1335
+ "pm.test('stale logout did not delete the live refresh token', () => pm.response.to.have.status(200));",
1336
+ "// WrapResponseInterceptor wraps every payload as",
1337
+ "// { statusCode, message, error, data }. Fall back to the raw body so this",
1338
+ "// also works against an app that does not register that interceptor.",
1339
+ "const body = pm.response.json();",
1340
+ "const b = body.data ?? body;",
1341
+ "pm.collectionVariables.set('accessToken2', b.accessToken);",
1342
+ "pm.collectionVariables.set('refreshToken2', b.refreshToken);"
1343
+ ]
1344
+ }
1345
+ }
1346
+ ],
1347
+ "request": {
1348
+ "method": "POST",
1349
+ "header": [
1350
+ {
1351
+ "key": "Content-Type",
1352
+ "value": "application/json"
1353
+ }
1354
+ ],
1355
+ "body": {
1356
+ "mode": "raw",
1357
+ "raw": "{\n \"refreshToken\": \"{{refreshToken2}}\"\n}"
1358
+ },
1359
+ "url": {
1360
+ "raw": "{{baseUrl}}/iam/refresh-tokens",
1361
+ "host": [
1362
+ "{{baseUrl}}"
1363
+ ],
1364
+ "path": [
1365
+ "iam",
1366
+ "refresh-tokens"
1367
+ ]
1368
+ }
1369
+ }
1370
+ },
1371
+ {
1372
+ "name": "D9. Client 2 logs out properly → 401",
1373
+ "event": [
1374
+ {
1375
+ "listen": "test",
1376
+ "script": {
1377
+ "type": "text/javascript",
1378
+ "exec": [
1379
+ "pm.test('genuine logout returns 200', () => pm.response.to.have.status(200));",
1380
+ "// Confirms the ownership check does not block legitimate logouts:",
1381
+ "// verified by the follow-up request D10."
1382
+ ]
1383
+ }
1384
+ }
1385
+ ],
1386
+ "request": {
1387
+ "method": "POST",
1388
+ "header": [
1389
+ {
1390
+ "key": "Content-Type",
1391
+ "value": "application/json"
1392
+ }
1393
+ ],
1394
+ "body": {
1395
+ "mode": "raw",
1396
+ "raw": "{\n \"refreshToken\": \"{{refreshToken2}}\"\n}"
1397
+ },
1398
+ "url": {
1399
+ "raw": "{{baseUrl}}/iam/logout",
1400
+ "host": [
1401
+ "{{baseUrl}}"
1402
+ ],
1403
+ "path": [
1404
+ "iam",
1405
+ "logout"
1406
+ ]
1407
+ }
1408
+ }
1409
+ },
1410
+ {
1411
+ "name": "D10. Client 2 token → 401 (genuine logout still revokes)",
1412
+ "event": [
1413
+ {
1414
+ "listen": "test",
1415
+ "script": {
1416
+ "type": "text/javascript",
1417
+ "exec": [
1418
+ "// The ownership check must not be so strict that real logouts no-op.",
1419
+ "pm.test('legitimate logout still revokes', () => pm.response.to.have.status(401));"
1420
+ ]
1421
+ }
1422
+ }
1423
+ ],
1424
+ "request": {
1425
+ "method": "GET",
1426
+ "header": [
1427
+ {
1428
+ "key": "Authorization",
1429
+ "value": "Bearer {{accessToken2}}"
1430
+ }
1431
+ ],
1432
+ "url": {
1433
+ "raw": "{{baseUrl}}/iam/me",
1434
+ "host": [
1435
+ "{{baseUrl}}"
1436
+ ],
1437
+ "path": [
1438
+ "iam",
1439
+ "me"
1440
+ ]
1441
+ }
1442
+ }
1443
+ }
1444
+ ]
1445
+ },
1446
+ {
1447
+ "name": "E — Teardown (always run this)",
1448
+ "item": [
1449
+ {
1450
+ "name": "E1. Reset both settings to false",
1451
+ "event": [
1452
+ {
1453
+ "listen": "test",
1454
+ "script": {
1455
+ "type": "text/javascript",
1456
+ "exec": [
1457
+ "pm.test('settings reset', () => pm.response.to.have.status(200));"
1458
+ ]
1459
+ }
1460
+ }
1461
+ ],
1462
+ "request": {
1463
+ "method": "PUT",
1464
+ "header": [
1465
+ {
1466
+ "key": "Content-Type",
1467
+ "value": "application/json"
1468
+ },
1469
+ {
1470
+ "key": "Authorization",
1471
+ "value": "Bearer {{adminToken}}"
1472
+ }
1473
+ ],
1474
+ "body": {
1475
+ "mode": "raw",
1476
+ "raw": "{\n \"settings\": [\n { \"key\": \"preventConcurrentLogins\", \"value\": \"false\", \"type\": \"system\" },\n { \"key\": \"invalidateAccessTokenOnLogout\", \"value\": \"false\", \"type\": \"system\" }\n ]\n}"
1477
+ },
1478
+ "url": {
1479
+ "raw": "{{baseUrl}}/setting/bulk",
1480
+ "host": [
1481
+ "{{baseUrl}}"
1482
+ ],
1483
+ "path": [
1484
+ "setting",
1485
+ "bulk"
1486
+ ]
1487
+ }
1488
+ }
1489
+ }
1490
+ ]
1491
+ }
1492
+ ]
1493
+ }