@qwickapps/server 1.4.0 → 1.5.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 (271) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +12 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -1
  6. package/dist/plugins/bans/bans-plugin.js +12 -3
  7. package/dist/plugins/bans/bans-plugin.js.map +1 -1
  8. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts +11 -0
  9. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts.map +1 -0
  10. package/dist/plugins/devices/__tests__/devices-plugin.test.js +410 -0
  11. package/dist/plugins/devices/__tests__/devices-plugin.test.js.map +1 -0
  12. package/dist/plugins/devices/__tests__/token-utils.test.d.ts +7 -0
  13. package/dist/plugins/devices/__tests__/token-utils.test.d.ts.map +1 -0
  14. package/dist/plugins/devices/__tests__/token-utils.test.js +197 -0
  15. package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -0
  16. package/dist/plugins/devices/adapters/compute-adapter.d.ts +36 -0
  17. package/dist/plugins/devices/adapters/compute-adapter.d.ts.map +1 -0
  18. package/dist/plugins/devices/adapters/compute-adapter.js +100 -0
  19. package/dist/plugins/devices/adapters/compute-adapter.js.map +1 -0
  20. package/dist/plugins/devices/adapters/index.d.ts +12 -0
  21. package/dist/plugins/devices/adapters/index.d.ts.map +1 -0
  22. package/dist/plugins/devices/adapters/index.js +10 -0
  23. package/dist/plugins/devices/adapters/index.js.map +1 -0
  24. package/dist/plugins/devices/adapters/mobile-adapter.d.ts +41 -0
  25. package/dist/plugins/devices/adapters/mobile-adapter.d.ts.map +1 -0
  26. package/dist/plugins/devices/adapters/mobile-adapter.js +131 -0
  27. package/dist/plugins/devices/adapters/mobile-adapter.js.map +1 -0
  28. package/dist/plugins/devices/devices-plugin.d.ts +70 -0
  29. package/dist/plugins/devices/devices-plugin.d.ts.map +1 -0
  30. package/dist/plugins/devices/devices-plugin.js +453 -0
  31. package/dist/plugins/devices/devices-plugin.js.map +1 -0
  32. package/dist/plugins/devices/index.d.ts +18 -0
  33. package/dist/plugins/devices/index.d.ts.map +1 -0
  34. package/dist/plugins/devices/index.js +18 -0
  35. package/dist/plugins/devices/index.js.map +1 -0
  36. package/dist/plugins/devices/stores/index.d.ts +9 -0
  37. package/dist/plugins/devices/stores/index.d.ts.map +1 -0
  38. package/dist/plugins/devices/stores/index.js +9 -0
  39. package/dist/plugins/devices/stores/index.js.map +1 -0
  40. package/dist/plugins/devices/stores/postgres-store.d.ts +26 -0
  41. package/dist/plugins/devices/stores/postgres-store.d.ts.map +1 -0
  42. package/dist/plugins/devices/stores/postgres-store.js +199 -0
  43. package/dist/plugins/devices/stores/postgres-store.js.map +1 -0
  44. package/dist/plugins/devices/token-utils.d.ts +100 -0
  45. package/dist/plugins/devices/token-utils.d.ts.map +1 -0
  46. package/dist/plugins/devices/token-utils.js +162 -0
  47. package/dist/plugins/devices/token-utils.js.map +1 -0
  48. package/dist/plugins/devices/types.d.ts +307 -0
  49. package/dist/plugins/devices/types.d.ts.map +1 -0
  50. package/dist/plugins/devices/types.js +10 -0
  51. package/dist/plugins/devices/types.js.map +1 -0
  52. package/dist/plugins/index.d.ts +14 -2
  53. package/dist/plugins/index.d.ts.map +1 -1
  54. package/dist/plugins/index.js +13 -1
  55. package/dist/plugins/index.js.map +1 -1
  56. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts +5 -0
  57. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts.map +1 -0
  58. package/dist/plugins/notifications/__tests__/notifications-manager.test.js +470 -0
  59. package/dist/plugins/notifications/__tests__/notifications-manager.test.js.map +1 -0
  60. package/dist/plugins/notifications/index.d.ts +71 -0
  61. package/dist/plugins/notifications/index.d.ts.map +1 -0
  62. package/dist/plugins/notifications/index.js +72 -0
  63. package/dist/plugins/notifications/index.js.map +1 -0
  64. package/dist/plugins/notifications/notifications-manager.d.ts +182 -0
  65. package/dist/plugins/notifications/notifications-manager.d.ts.map +1 -0
  66. package/dist/plugins/notifications/notifications-manager.js +610 -0
  67. package/dist/plugins/notifications/notifications-manager.js.map +1 -0
  68. package/dist/plugins/notifications/notifications-plugin.d.ts +83 -0
  69. package/dist/plugins/notifications/notifications-plugin.d.ts.map +1 -0
  70. package/dist/plugins/notifications/notifications-plugin.js +337 -0
  71. package/dist/plugins/notifications/notifications-plugin.js.map +1 -0
  72. package/dist/plugins/notifications/types.d.ts +164 -0
  73. package/dist/plugins/notifications/types.d.ts.map +1 -0
  74. package/dist/plugins/notifications/types.js +9 -0
  75. package/dist/plugins/notifications/types.js.map +1 -0
  76. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts +12 -0
  77. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts.map +1 -0
  78. package/dist/plugins/parental/__tests__/parental-plugin.test.js +349 -0
  79. package/dist/plugins/parental/__tests__/parental-plugin.test.js.map +1 -0
  80. package/dist/plugins/parental/adapters/index.d.ts +8 -0
  81. package/dist/plugins/parental/adapters/index.d.ts.map +1 -0
  82. package/dist/plugins/parental/adapters/index.js +7 -0
  83. package/dist/plugins/parental/adapters/index.js.map +1 -0
  84. package/dist/plugins/parental/adapters/kids-adapter.d.ts +24 -0
  85. package/dist/plugins/parental/adapters/kids-adapter.d.ts.map +1 -0
  86. package/dist/plugins/parental/adapters/kids-adapter.js +174 -0
  87. package/dist/plugins/parental/adapters/kids-adapter.js.map +1 -0
  88. package/dist/plugins/parental/index.d.ts +14 -0
  89. package/dist/plugins/parental/index.d.ts.map +1 -0
  90. package/dist/plugins/parental/index.js +15 -0
  91. package/dist/plugins/parental/index.js.map +1 -0
  92. package/dist/plugins/parental/parental-plugin.d.ts +88 -0
  93. package/dist/plugins/parental/parental-plugin.d.ts.map +1 -0
  94. package/dist/plugins/parental/parental-plugin.js +666 -0
  95. package/dist/plugins/parental/parental-plugin.js.map +1 -0
  96. package/dist/plugins/parental/stores/index.d.ts +7 -0
  97. package/dist/plugins/parental/stores/index.d.ts.map +1 -0
  98. package/dist/plugins/parental/stores/index.js +7 -0
  99. package/dist/plugins/parental/stores/index.js.map +1 -0
  100. package/dist/plugins/parental/stores/postgres-store.d.ts +10 -0
  101. package/dist/plugins/parental/stores/postgres-store.d.ts.map +1 -0
  102. package/dist/plugins/parental/stores/postgres-store.js +209 -0
  103. package/dist/plugins/parental/stores/postgres-store.js.map +1 -0
  104. package/dist/plugins/parental/types.d.ts +154 -0
  105. package/dist/plugins/parental/types.d.ts.map +1 -0
  106. package/dist/plugins/parental/types.js +10 -0
  107. package/dist/plugins/parental/types.js.map +1 -0
  108. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts +11 -0
  109. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts.map +1 -0
  110. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js +243 -0
  111. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js.map +1 -0
  112. package/dist/plugins/profiles/index.d.ts +12 -0
  113. package/dist/plugins/profiles/index.d.ts.map +1 -0
  114. package/dist/plugins/profiles/index.js +13 -0
  115. package/dist/plugins/profiles/index.js.map +1 -0
  116. package/dist/plugins/profiles/profiles-plugin.d.ts +71 -0
  117. package/dist/plugins/profiles/profiles-plugin.d.ts.map +1 -0
  118. package/dist/plugins/profiles/profiles-plugin.js +481 -0
  119. package/dist/plugins/profiles/profiles-plugin.js.map +1 -0
  120. package/dist/plugins/profiles/stores/index.d.ts +9 -0
  121. package/dist/plugins/profiles/stores/index.d.ts.map +1 -0
  122. package/dist/plugins/profiles/stores/index.js +9 -0
  123. package/dist/plugins/profiles/stores/index.js.map +1 -0
  124. package/dist/plugins/profiles/stores/postgres-store.d.ts +18 -0
  125. package/dist/plugins/profiles/stores/postgres-store.d.ts.map +1 -0
  126. package/dist/plugins/profiles/stores/postgres-store.js +310 -0
  127. package/dist/plugins/profiles/stores/postgres-store.js.map +1 -0
  128. package/dist/plugins/profiles/types.d.ts +289 -0
  129. package/dist/plugins/profiles/types.d.ts.map +1 -0
  130. package/dist/plugins/profiles/types.js +10 -0
  131. package/dist/plugins/profiles/types.js.map +1 -0
  132. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts +11 -0
  133. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts.map +1 -0
  134. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js +305 -0
  135. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js.map +1 -0
  136. package/dist/plugins/subscriptions/index.d.ts +12 -0
  137. package/dist/plugins/subscriptions/index.d.ts.map +1 -0
  138. package/dist/plugins/subscriptions/index.js +13 -0
  139. package/dist/plugins/subscriptions/index.js.map +1 -0
  140. package/dist/plugins/subscriptions/stores/index.d.ts +9 -0
  141. package/dist/plugins/subscriptions/stores/index.d.ts.map +1 -0
  142. package/dist/plugins/subscriptions/stores/index.js +9 -0
  143. package/dist/plugins/subscriptions/stores/index.js.map +1 -0
  144. package/dist/plugins/subscriptions/stores/postgres-store.d.ts +14 -0
  145. package/dist/plugins/subscriptions/stores/postgres-store.d.ts.map +1 -0
  146. package/dist/plugins/subscriptions/stores/postgres-store.js +359 -0
  147. package/dist/plugins/subscriptions/stores/postgres-store.js.map +1 -0
  148. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts +82 -0
  149. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts.map +1 -0
  150. package/dist/plugins/subscriptions/subscriptions-plugin.js +449 -0
  151. package/dist/plugins/subscriptions/subscriptions-plugin.js.map +1 -0
  152. package/dist/plugins/subscriptions/types.d.ts +308 -0
  153. package/dist/plugins/subscriptions/types.d.ts.map +1 -0
  154. package/dist/plugins/subscriptions/types.js +10 -0
  155. package/dist/plugins/subscriptions/types.js.map +1 -0
  156. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts +11 -0
  157. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts.map +1 -0
  158. package/dist/plugins/usage/__tests__/usage-plugin.test.js +218 -0
  159. package/dist/plugins/usage/__tests__/usage-plugin.test.js.map +1 -0
  160. package/dist/plugins/usage/index.d.ts +12 -0
  161. package/dist/plugins/usage/index.d.ts.map +1 -0
  162. package/dist/plugins/usage/index.js +13 -0
  163. package/dist/plugins/usage/index.js.map +1 -0
  164. package/dist/plugins/usage/stores/index.d.ts +9 -0
  165. package/dist/plugins/usage/stores/index.d.ts.map +1 -0
  166. package/dist/plugins/usage/stores/index.js +9 -0
  167. package/dist/plugins/usage/stores/index.js.map +1 -0
  168. package/dist/plugins/usage/stores/postgres-store.d.ts +14 -0
  169. package/dist/plugins/usage/stores/postgres-store.d.ts.map +1 -0
  170. package/dist/plugins/usage/stores/postgres-store.js +146 -0
  171. package/dist/plugins/usage/stores/postgres-store.js.map +1 -0
  172. package/dist/plugins/usage/types.d.ts +195 -0
  173. package/dist/plugins/usage/types.d.ts.map +1 -0
  174. package/dist/plugins/usage/types.js +10 -0
  175. package/dist/plugins/usage/types.js.map +1 -0
  176. package/dist/plugins/usage/usage-plugin.d.ts +51 -0
  177. package/dist/plugins/usage/usage-plugin.d.ts.map +1 -0
  178. package/dist/plugins/usage/usage-plugin.js +412 -0
  179. package/dist/plugins/usage/usage-plugin.js.map +1 -0
  180. package/dist/plugins/users/__tests__/postgres-store.test.d.ts +10 -0
  181. package/dist/plugins/users/__tests__/postgres-store.test.d.ts.map +1 -0
  182. package/dist/plugins/users/__tests__/postgres-store.test.js +229 -0
  183. package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -0
  184. package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
  185. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
  186. package/dist/plugins/users/index.d.ts +2 -2
  187. package/dist/plugins/users/index.d.ts.map +1 -1
  188. package/dist/plugins/users/index.js +1 -1
  189. package/dist/plugins/users/index.js.map +1 -1
  190. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
  191. package/dist/plugins/users/stores/postgres-store.js +76 -0
  192. package/dist/plugins/users/stores/postgres-store.js.map +1 -1
  193. package/dist/plugins/users/types.d.ts +74 -6
  194. package/dist/plugins/users/types.d.ts.map +1 -1
  195. package/dist/plugins/users/users-plugin.d.ts +15 -1
  196. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  197. package/dist/plugins/users/users-plugin.js +29 -0
  198. package/dist/plugins/users/users-plugin.js.map +1 -1
  199. package/dist-ui/assets/index-CynOqPkb.js +469 -0
  200. package/dist-ui/assets/index-CynOqPkb.js.map +1 -0
  201. package/dist-ui/index.html +1 -1
  202. package/dist-ui-lib/api/controlPanelApi.d.ts +46 -0
  203. package/dist-ui-lib/components/StatCard.d.ts +16 -0
  204. package/dist-ui-lib/dashboard/widgets/NotificationsStatsWidget.d.ts +12 -0
  205. package/dist-ui-lib/dashboard/widgets/index.d.ts +1 -0
  206. package/dist-ui-lib/index.js +1822 -1611
  207. package/dist-ui-lib/index.js.map +1 -1
  208. package/dist-ui-lib/pages/NotificationsPage.d.ts +9 -0
  209. package/dist-ui-lib/utils/formatters.d.ts +19 -0
  210. package/package.json +1 -1
  211. package/src/index.ts +178 -0
  212. package/src/plugins/bans/bans-plugin.ts +15 -3
  213. package/src/plugins/devices/__tests__/devices-plugin.test.ts +551 -0
  214. package/src/plugins/devices/__tests__/token-utils.test.ts +264 -0
  215. package/src/plugins/devices/adapters/compute-adapter.ts +139 -0
  216. package/src/plugins/devices/adapters/index.ts +13 -0
  217. package/src/plugins/devices/adapters/mobile-adapter.ts +179 -0
  218. package/src/plugins/devices/devices-plugin.ts +538 -0
  219. package/src/plugins/devices/index.ts +69 -0
  220. package/src/plugins/devices/stores/index.ts +9 -0
  221. package/src/plugins/devices/stores/postgres-store.ts +304 -0
  222. package/src/plugins/devices/token-utils.ts +213 -0
  223. package/src/plugins/devices/types.ts +351 -0
  224. package/src/plugins/index.ts +218 -0
  225. package/src/plugins/notifications/__tests__/notifications-manager.test.ts +637 -0
  226. package/src/plugins/notifications/index.ts +91 -0
  227. package/src/plugins/notifications/notifications-manager.ts +773 -0
  228. package/src/plugins/notifications/notifications-plugin.ts +398 -0
  229. package/src/plugins/notifications/types.ts +207 -0
  230. package/src/plugins/parental/__tests__/parental-plugin.test.ts +465 -0
  231. package/src/plugins/parental/adapters/index.ts +8 -0
  232. package/src/plugins/parental/adapters/kids-adapter.ts +206 -0
  233. package/src/plugins/parental/index.ts +55 -0
  234. package/src/plugins/parental/parental-plugin.ts +759 -0
  235. package/src/plugins/parental/stores/index.ts +7 -0
  236. package/src/plugins/parental/stores/postgres-store.ts +304 -0
  237. package/src/plugins/parental/types.ts +180 -0
  238. package/src/plugins/profiles/__tests__/profiles-plugin.test.ts +321 -0
  239. package/src/plugins/profiles/index.ts +49 -0
  240. package/src/plugins/profiles/profiles-plugin.ts +546 -0
  241. package/src/plugins/profiles/stores/index.ts +9 -0
  242. package/src/plugins/profiles/stores/postgres-store.ts +439 -0
  243. package/src/plugins/profiles/types.ts +338 -0
  244. package/src/plugins/subscriptions/__tests__/subscriptions-plugin.test.ts +404 -0
  245. package/src/plugins/subscriptions/index.ts +51 -0
  246. package/src/plugins/subscriptions/stores/index.ts +9 -0
  247. package/src/plugins/subscriptions/stores/postgres-store.ts +482 -0
  248. package/src/plugins/subscriptions/subscriptions-plugin.ts +530 -0
  249. package/src/plugins/subscriptions/types.ts +355 -0
  250. package/src/plugins/usage/__tests__/usage-plugin.test.ts +288 -0
  251. package/src/plugins/usage/index.ts +39 -0
  252. package/src/plugins/usage/stores/index.ts +9 -0
  253. package/src/plugins/usage/stores/postgres-store.ts +213 -0
  254. package/src/plugins/usage/types.ts +222 -0
  255. package/src/plugins/usage/usage-plugin.ts +484 -0
  256. package/src/plugins/users/__tests__/postgres-store.test.ts +326 -0
  257. package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
  258. package/src/plugins/users/index.ts +6 -0
  259. package/src/plugins/users/stores/postgres-store.ts +104 -0
  260. package/src/plugins/users/types.ts +82 -6
  261. package/src/plugins/users/users-plugin.ts +37 -0
  262. package/ui/src/App.tsx +5 -1
  263. package/ui/src/api/controlPanelApi.ts +103 -6
  264. package/ui/src/components/StatCard.tsx +58 -0
  265. package/ui/src/dashboard/builtInWidgets.tsx +3 -1
  266. package/ui/src/dashboard/widgets/NotificationsStatsWidget.tsx +167 -0
  267. package/ui/src/dashboard/widgets/index.ts +1 -0
  268. package/ui/src/pages/NotificationsPage.tsx +417 -0
  269. package/ui/src/utils/formatters.ts +33 -0
  270. package/dist-ui/assets/index-D7DoZ9rL.js +0 -478
  271. package/dist-ui/assets/index-D7DoZ9rL.js.map +0 -1
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Token Utilities Tests
3
+ *
4
+ * Unit tests for device token generation, hashing, and verification.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import {
9
+ generateDeviceToken,
10
+ generatePairingCode,
11
+ hashToken,
12
+ verifyToken,
13
+ isValidTokenFormat,
14
+ isTokenExpired,
15
+ getTokenExpiration,
16
+ } from '../token-utils.js';
17
+
18
+ describe('Token Utilities', () => {
19
+ describe('generateDeviceToken', () => {
20
+ it('should generate a token with the correct prefix', async () => {
21
+ const result = await generateDeviceToken('mob');
22
+ expect(result.token).toMatch(/^mob_/);
23
+ });
24
+
25
+ it('should generate a 43-character secret after prefix', async () => {
26
+ const result = await generateDeviceToken('mob');
27
+ const secret = result.token.split('_')[1];
28
+ expect(secret.length).toBe(43);
29
+ });
30
+
31
+ it('should generate base64url-safe characters', async () => {
32
+ const result = await generateDeviceToken('test');
33
+ const secret = result.token.split('_')[1];
34
+ expect(secret).toMatch(/^[A-Za-z0-9_-]+$/);
35
+ });
36
+
37
+ it('should return a SHA-256 hash (64 hex characters)', async () => {
38
+ const result = await generateDeviceToken('mob');
39
+ expect(result.hash).toMatch(/^[a-f0-9]{64}$/);
40
+ });
41
+
42
+ it('should return an 8-character prefix for display', async () => {
43
+ const result = await generateDeviceToken('mob');
44
+ expect(result.prefix.length).toBe(8);
45
+ expect(result.token.startsWith(result.prefix)).toBe(true);
46
+ });
47
+
48
+ it('should generate unique tokens each time', async () => {
49
+ const result1 = await generateDeviceToken('mob');
50
+ const result2 = await generateDeviceToken('mob');
51
+
52
+ expect(result1.token).not.toBe(result2.token);
53
+ expect(result1.hash).not.toBe(result2.hash);
54
+ });
55
+ });
56
+
57
+ describe('generatePairingCode', () => {
58
+ it('should generate a 6-character code', () => {
59
+ const code = generatePairingCode();
60
+ expect(code.length).toBe(6);
61
+ });
62
+
63
+ it('should only contain uppercase alphanumeric characters', () => {
64
+ const code = generatePairingCode();
65
+ // Excludes confusing chars: 0, O, I, 1
66
+ expect(code).toMatch(/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]+$/);
67
+ });
68
+
69
+ it('should generate unique codes', () => {
70
+ const codes = new Set<string>();
71
+ for (let i = 0; i < 100; i++) {
72
+ codes.add(generatePairingCode());
73
+ }
74
+ // Should have mostly unique codes (allowing some collisions)
75
+ expect(codes.size).toBeGreaterThan(90);
76
+ });
77
+ });
78
+
79
+ describe('hashToken', () => {
80
+ it('should return a 64-character hex string', async () => {
81
+ const hash = await hashToken('test_token');
82
+ expect(hash.length).toBe(64);
83
+ expect(hash).toMatch(/^[a-f0-9]+$/);
84
+ });
85
+
86
+ it('should produce consistent hashes for same input', async () => {
87
+ const hash1 = await hashToken('same_token');
88
+ const hash2 = await hashToken('same_token');
89
+ expect(hash1).toBe(hash2);
90
+ });
91
+
92
+ it('should produce different hashes for different input', async () => {
93
+ const hash1 = await hashToken('token_one');
94
+ const hash2 = await hashToken('token_two');
95
+ expect(hash1).not.toBe(hash2);
96
+ });
97
+ });
98
+
99
+ describe('verifyToken', () => {
100
+ it('should return valid for matching token and hash', async () => {
101
+ const token = 'mob_ABC123testtoken';
102
+ const hash = await hashToken(token);
103
+
104
+ const result = await verifyToken(token, hash);
105
+
106
+ expect(result.valid).toBe(true);
107
+ expect(result.error).toBeUndefined();
108
+ });
109
+
110
+ it('should return invalid for non-matching token', async () => {
111
+ const originalHash = await hashToken('original_token');
112
+
113
+ const result = await verifyToken('different_token', originalHash);
114
+
115
+ expect(result.valid).toBe(false);
116
+ expect(result.error).toBe('Invalid token');
117
+ });
118
+
119
+ it('should handle malformed hash gracefully', async () => {
120
+ const result = await verifyToken('some_token', 'not-a-valid-hex-hash');
121
+
122
+ expect(result.valid).toBe(false);
123
+ expect(result.error).toBeDefined();
124
+ });
125
+
126
+ it('should use constant-time comparison (timing-safe)', async () => {
127
+ // This test ensures the comparison doesn't leak timing info
128
+ // We can't directly test timing, but we verify the function uses the correct approach
129
+ const token = 'mob_securetoken123';
130
+ const hash = await hashToken(token);
131
+
132
+ // Multiple verifications should all succeed
133
+ for (let i = 0; i < 10; i++) {
134
+ const result = await verifyToken(token, hash);
135
+ expect(result.valid).toBe(true);
136
+ }
137
+ });
138
+ });
139
+
140
+ describe('isValidTokenFormat', () => {
141
+ it('should return true for valid token format', () => {
142
+ // 43 base64url characters after prefix (32 bytes = 43 chars in base64url)
143
+ const validToken = 'mob_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk123456';
144
+ expect(isValidTokenFormat(validToken, 'mob')).toBe(true);
145
+ });
146
+
147
+ it('should return false for wrong prefix', () => {
148
+ const token = 'mob_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk123456';
149
+ expect(isValidTokenFormat(token, 'cpute')).toBe(false);
150
+ });
151
+
152
+ it('should return false for missing underscore', () => {
153
+ const token = 'mobABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk123456';
154
+ expect(isValidTokenFormat(token, 'mob')).toBe(false);
155
+ });
156
+
157
+ it('should return false for too short secret', () => {
158
+ const token = 'mob_tooshort';
159
+ expect(isValidTokenFormat(token, 'mob')).toBe(false);
160
+ });
161
+
162
+ it('should return false for too long secret', () => {
163
+ const token = 'mob_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk123456extra';
164
+ expect(isValidTokenFormat(token, 'mob')).toBe(false);
165
+ });
166
+
167
+ it('should return false for invalid characters', () => {
168
+ const token = 'mob_ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()123456';
169
+ expect(isValidTokenFormat(token, 'mob')).toBe(false);
170
+ });
171
+
172
+ it('should accept base64url characters including - and _', () => {
173
+ const token = 'mob_ABCDEFGHIJKLMNOPQRSTUVWXYZ-_abcdefghij12345';
174
+ expect(isValidTokenFormat(token, 'mob')).toBe(true);
175
+ });
176
+ });
177
+
178
+ describe('isTokenExpired', () => {
179
+ it('should return false for future date', () => {
180
+ const futureDate = new Date();
181
+ futureDate.setDate(futureDate.getDate() + 30);
182
+
183
+ expect(isTokenExpired(futureDate)).toBe(false);
184
+ });
185
+
186
+ it('should return true for past date', () => {
187
+ const pastDate = new Date();
188
+ pastDate.setDate(pastDate.getDate() - 1);
189
+
190
+ expect(isTokenExpired(pastDate)).toBe(true);
191
+ });
192
+
193
+ it('should return true for date exactly now (edge case)', () => {
194
+ const now = new Date();
195
+ // Slightly in the past to ensure it's expired
196
+ now.setMilliseconds(now.getMilliseconds() - 1);
197
+
198
+ expect(isTokenExpired(now)).toBe(true);
199
+ });
200
+ });
201
+
202
+ describe('getTokenExpiration', () => {
203
+ it('should return a date 90 days in the future by default', () => {
204
+ const now = new Date();
205
+ const expiration = getTokenExpiration();
206
+
207
+ const daysDiff = Math.round(
208
+ (expiration.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
209
+ );
210
+
211
+ expect(daysDiff).toBe(90);
212
+ });
213
+
214
+ it('should accept custom validity days', () => {
215
+ const now = new Date();
216
+ const expiration = getTokenExpiration(30);
217
+
218
+ const daysDiff = Math.round(
219
+ (expiration.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
220
+ );
221
+
222
+ expect(daysDiff).toBe(30);
223
+ });
224
+
225
+ it('should handle 0 days (expires immediately)', () => {
226
+ const now = new Date();
227
+ const expiration = getTokenExpiration(0);
228
+
229
+ // Should be very close to now
230
+ const diffMs = Math.abs(expiration.getTime() - now.getTime());
231
+ expect(diffMs).toBeLessThan(1000); // Within 1 second
232
+ });
233
+
234
+ it('should handle large values', () => {
235
+ const now = new Date();
236
+ const expiration = getTokenExpiration(365);
237
+
238
+ const daysDiff = Math.round(
239
+ (expiration.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
240
+ );
241
+
242
+ expect(daysDiff).toBe(365);
243
+ });
244
+ });
245
+
246
+ describe('integration: generate and verify', () => {
247
+ it('should generate token that can be verified', async () => {
248
+ const { token, hash } = await generateDeviceToken('mob');
249
+
250
+ const verifyResult = await verifyToken(token, hash);
251
+
252
+ expect(verifyResult.valid).toBe(true);
253
+ });
254
+
255
+ it('should reject tampered token', async () => {
256
+ const { hash } = await generateDeviceToken('mob');
257
+ const tamperedToken = 'mob_tamperedtokenthatisdifferentfromoriginal';
258
+
259
+ const verifyResult = await verifyToken(tamperedToken, hash);
260
+
261
+ expect(verifyResult.valid).toBe(false);
262
+ });
263
+ });
264
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Compute Device Adapter
3
+ *
4
+ * Adapter for computing devices (laptops, desktops, containers).
5
+ * Used by QwickForge for CLI device management.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ import type {
11
+ DeviceAdapter,
12
+ CreateDeviceInput,
13
+ ValidationResult,
14
+ Device,
15
+ ComputeDeviceMetadata,
16
+ } from '../types.js';
17
+
18
+ /**
19
+ * Compute adapter configuration
20
+ */
21
+ export interface ComputeAdapterConfig {
22
+ /** Token prefix (default: 'qwf_dev') */
23
+ tokenPrefix?: string;
24
+ /** Require hostname in metadata */
25
+ requireHostname?: boolean;
26
+ /** Allowed OS types (optional, allows all if not specified) */
27
+ allowedOS?: string[];
28
+ }
29
+
30
+ /**
31
+ * Create a compute device adapter
32
+ *
33
+ * @param config - Adapter configuration
34
+ * @returns DeviceAdapter for compute devices
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const adapter = computeDeviceAdapter({
39
+ * tokenPrefix: 'qwf_dev',
40
+ * requireHostname: true,
41
+ * });
42
+ * ```
43
+ */
44
+ export function computeDeviceAdapter(config: ComputeAdapterConfig = {}): DeviceAdapter {
45
+ const {
46
+ tokenPrefix = 'qwf_dev',
47
+ requireHostname = false,
48
+ allowedOS,
49
+ } = config;
50
+
51
+ return {
52
+ name: 'compute',
53
+ tokenPrefix,
54
+
55
+ validateDeviceInput(input: CreateDeviceInput): ValidationResult {
56
+ const errors: string[] = [];
57
+ const metadata = input.metadata as ComputeDeviceMetadata | undefined;
58
+
59
+ // Validate name
60
+ if (!input.name || input.name.trim().length === 0) {
61
+ errors.push('Device name is required');
62
+ }
63
+
64
+ if (input.name && input.name.length > 255) {
65
+ errors.push('Device name must be 255 characters or less');
66
+ }
67
+
68
+ // Validate hostname if required
69
+ if (requireHostname && (!metadata?.hostname || metadata.hostname.trim().length === 0)) {
70
+ errors.push('Hostname is required for compute devices');
71
+ }
72
+
73
+ // Validate OS if allowedOS is specified
74
+ if (allowedOS && metadata?.os) {
75
+ if (!allowedOS.includes(metadata.os)) {
76
+ errors.push(`OS '${metadata.os}' is not allowed. Allowed: ${allowedOS.join(', ')}`);
77
+ }
78
+ }
79
+
80
+ // Validate architecture
81
+ if (metadata?.arch) {
82
+ const validArchs = ['x64', 'arm64', 'ia32', 'arm'];
83
+ if (!validArchs.includes(metadata.arch)) {
84
+ errors.push(`Invalid architecture '${metadata.arch}'. Valid: ${validArchs.join(', ')}`);
85
+ }
86
+ }
87
+
88
+ return {
89
+ valid: errors.length === 0,
90
+ errors: errors.length > 0 ? errors : undefined,
91
+ };
92
+ },
93
+
94
+ transformForStorage(input: CreateDeviceInput): Record<string, unknown> {
95
+ const metadata = (input.metadata || {}) as ComputeDeviceMetadata;
96
+
97
+ // Ensure compute-specific defaults
98
+ return {
99
+ hostname: metadata.hostname || null,
100
+ os: metadata.os || null,
101
+ os_version: metadata.os_version || null,
102
+ arch: metadata.arch || null,
103
+ cli_capabilities: metadata.cli_capabilities || [],
104
+ container_id: metadata.container_id || null,
105
+ node_version: metadata.node_version || null,
106
+ };
107
+ },
108
+
109
+ transformFromStorage(row: Record<string, unknown>): Record<string, unknown> {
110
+ const metadata = (row.metadata || {}) as Record<string, unknown>;
111
+
112
+ // Extract compute-specific fields from metadata
113
+ return {
114
+ hostname: metadata.hostname,
115
+ os: metadata.os,
116
+ os_version: metadata.os_version,
117
+ arch: metadata.arch,
118
+ cli_capabilities: metadata.cli_capabilities || [],
119
+ container_id: metadata.container_id,
120
+ node_version: metadata.node_version,
121
+ };
122
+ },
123
+
124
+ async onDeviceCreated(device: Device): Promise<void> {
125
+ // Optional: Log device creation, send notifications, etc.
126
+ console.log(`[ComputeAdapter] Device created: ${device.name} (${device.id})`);
127
+ },
128
+
129
+ async onDeviceDeleted(device: Device): Promise<void> {
130
+ // Optional: Cleanup, revoke certificates, etc.
131
+ console.log(`[ComputeAdapter] Device deleted: ${device.name} (${device.id})`);
132
+ },
133
+
134
+ async onDeviceVerified(device: Device, ip?: string): Promise<void> {
135
+ // Optional: Log device verification, track activity
136
+ console.log(`[ComputeAdapter] Device verified: ${device.name} from ${ip || 'unknown'}`);
137
+ },
138
+ };
139
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Device Adapters
3
+ *
4
+ * Export all built-in device adapters.
5
+ *
6
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
+ */
8
+
9
+ export { computeDeviceAdapter } from './compute-adapter.js';
10
+ export type { ComputeAdapterConfig } from './compute-adapter.js';
11
+
12
+ export { mobileDeviceAdapter } from './mobile-adapter.js';
13
+ export type { MobileAdapterConfig } from './mobile-adapter.js';
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Mobile Device Adapter
3
+ *
4
+ * Adapter for mobile devices (phones, tablets).
5
+ * Used by QwickBot for mobile app device management.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ import type {
11
+ DeviceAdapter,
12
+ CreateDeviceInput,
13
+ ValidationResult,
14
+ Device,
15
+ MobileDeviceMetadata,
16
+ } from '../types.js';
17
+
18
+ /**
19
+ * Mobile adapter configuration
20
+ */
21
+ export interface MobileAdapterConfig {
22
+ /** Token prefix (default: 'qwb_mob') */
23
+ tokenPrefix?: string;
24
+ /** Require device model in metadata */
25
+ requireDeviceModel?: boolean;
26
+ /** Require app version in metadata */
27
+ requireAppVersion?: boolean;
28
+ /** Allowed OS names (optional, allows all if not specified) */
29
+ allowedOS?: string[];
30
+ /** Minimum app version (optional, uses semver) */
31
+ minAppVersion?: string;
32
+ }
33
+
34
+ /**
35
+ * Compare semver versions (simplified)
36
+ */
37
+ function compareSemver(v1: string, v2: string): number {
38
+ const parts1 = v1.split('.').map(Number);
39
+ const parts2 = v2.split('.').map(Number);
40
+
41
+ for (let i = 0; i < 3; i++) {
42
+ const p1 = parts1[i] || 0;
43
+ const p2 = parts2[i] || 0;
44
+ if (p1 > p2) return 1;
45
+ if (p1 < p2) return -1;
46
+ }
47
+ return 0;
48
+ }
49
+
50
+ /**
51
+ * Create a mobile device adapter
52
+ *
53
+ * @param config - Adapter configuration
54
+ * @returns DeviceAdapter for mobile devices
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const adapter = mobileDeviceAdapter({
59
+ * tokenPrefix: 'qwb_mob',
60
+ * requireDeviceModel: true,
61
+ * allowedOS: ['iOS', 'Android'],
62
+ * });
63
+ * ```
64
+ */
65
+ export function mobileDeviceAdapter(config: MobileAdapterConfig = {}): DeviceAdapter {
66
+ const {
67
+ tokenPrefix = 'qwb_mob',
68
+ requireDeviceModel = false,
69
+ requireAppVersion = false,
70
+ allowedOS,
71
+ minAppVersion,
72
+ } = config;
73
+
74
+ return {
75
+ name: 'mobile',
76
+ tokenPrefix,
77
+
78
+ validateDeviceInput(input: CreateDeviceInput): ValidationResult {
79
+ const errors: string[] = [];
80
+ const metadata = input.metadata as MobileDeviceMetadata | undefined;
81
+
82
+ // Validate name
83
+ if (!input.name || input.name.trim().length === 0) {
84
+ errors.push('Device name is required');
85
+ }
86
+
87
+ if (input.name && input.name.length > 255) {
88
+ errors.push('Device name must be 255 characters or less');
89
+ }
90
+
91
+ // Validate device model if required
92
+ if (requireDeviceModel && (!metadata?.device_model || metadata.device_model.trim().length === 0)) {
93
+ errors.push('Device model is required for mobile devices');
94
+ }
95
+
96
+ // Validate app version if required
97
+ if (requireAppVersion && (!metadata?.app_version || metadata.app_version.trim().length === 0)) {
98
+ errors.push('App version is required for mobile devices');
99
+ }
100
+
101
+ // Validate OS if allowedOS is specified
102
+ if (allowedOS && metadata?.os_name) {
103
+ if (!allowedOS.includes(metadata.os_name)) {
104
+ errors.push(`OS '${metadata.os_name}' is not supported. Supported: ${allowedOS.join(', ')}`);
105
+ }
106
+ }
107
+
108
+ // Validate minimum app version
109
+ if (minAppVersion && metadata?.app_version) {
110
+ if (compareSemver(metadata.app_version, minAppVersion) < 0) {
111
+ errors.push(`App version ${metadata.app_version} is below minimum required version ${minAppVersion}`);
112
+ }
113
+ }
114
+
115
+ // Validate screen dimensions if provided
116
+ if (metadata?.screen_width !== undefined && metadata.screen_width <= 0) {
117
+ errors.push('Screen width must be a positive number');
118
+ }
119
+
120
+ if (metadata?.screen_height !== undefined && metadata.screen_height <= 0) {
121
+ errors.push('Screen height must be a positive number');
122
+ }
123
+
124
+ return {
125
+ valid: errors.length === 0,
126
+ errors: errors.length > 0 ? errors : undefined,
127
+ };
128
+ },
129
+
130
+ transformForStorage(input: CreateDeviceInput): Record<string, unknown> {
131
+ const metadata = (input.metadata || {}) as MobileDeviceMetadata;
132
+
133
+ // Ensure mobile-specific defaults
134
+ return {
135
+ device_model: metadata.device_model || null,
136
+ os_name: metadata.os_name || null,
137
+ os_version: metadata.os_version || null,
138
+ app_version: metadata.app_version || null,
139
+ push_token: metadata.push_token || null,
140
+ screen_width: metadata.screen_width || null,
141
+ screen_height: metadata.screen_height || null,
142
+ };
143
+ },
144
+
145
+ transformFromStorage(row: Record<string, unknown>): Record<string, unknown> {
146
+ const metadata = (row.metadata || {}) as Record<string, unknown>;
147
+
148
+ // Extract mobile-specific fields from metadata
149
+ return {
150
+ device_model: metadata.device_model,
151
+ os_name: metadata.os_name,
152
+ os_version: metadata.os_version,
153
+ app_version: metadata.app_version,
154
+ push_token: metadata.push_token,
155
+ screen_width: metadata.screen_width,
156
+ screen_height: metadata.screen_height,
157
+ };
158
+ },
159
+
160
+ async onDeviceCreated(device: Device): Promise<void> {
161
+ // Optional: Log device creation, track in analytics, etc.
162
+ console.log(`[MobileAdapter] Device registered: ${device.name} (${device.id})`);
163
+ },
164
+
165
+ async onDeviceDeleted(device: Device): Promise<void> {
166
+ // Optional: Unregister push token, cleanup, etc.
167
+ const metadata = device.metadata as MobileDeviceMetadata;
168
+ if (metadata?.push_token) {
169
+ console.log(`[MobileAdapter] Should unregister push token for device: ${device.id}`);
170
+ }
171
+ console.log(`[MobileAdapter] Device deleted: ${device.name} (${device.id})`);
172
+ },
173
+
174
+ async onDeviceVerified(device: Device, ip?: string): Promise<void> {
175
+ // Optional: Log device verification, track activity
176
+ console.log(`[MobileAdapter] Device verified: ${device.name} from ${ip || 'unknown'}`);
177
+ },
178
+ };
179
+ }