@harperfast/harper-pro 5.0.0-alpha.2 → 5.0.0-alpha.4

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 (226) hide show
  1. package/core/.github/workflows/create-release.yaml +181 -0
  2. package/core/.github/workflows/notify-release-published.yaml +50 -0
  3. package/core/.github/workflows/publish-docker.yaml +174 -0
  4. package/core/.github/workflows/publish-npm.yaml +173 -0
  5. package/core/CONTRIBUTING.md +2 -0
  6. package/core/bin/harper.js +6 -0
  7. package/core/components/PluginModule.ts +0 -1
  8. package/core/components/componentLoader.ts +16 -4
  9. package/core/dev/sync-commits.js +18 -7
  10. package/core/integrationTests/server/operation-user-rbac.test.ts +484 -0
  11. package/core/package.json +4 -3
  12. package/core/resources/DatabaseTransaction.ts +2 -1
  13. package/core/resources/LMDBTransaction.ts +9 -4
  14. package/core/resources/RecordEncoder.ts +5 -3
  15. package/core/resources/RequestTarget.ts +1 -1
  16. package/core/resources/Resource.ts +1 -1
  17. package/core/resources/RocksTransactionLogStore.ts +106 -12
  18. package/core/resources/Table.ts +108 -114
  19. package/core/resources/dataLoader.ts +0 -1
  20. package/core/resources/databases.ts +1 -1
  21. package/core/resources/jsResource.ts +0 -2
  22. package/core/resources/registrationDeprecated.ts +8 -0
  23. package/core/resources/transactionBroadcast.ts +29 -25
  24. package/core/security/auth.ts +14 -1
  25. package/core/security/jsLoader.ts +9 -2
  26. package/core/security/permissionsTranslator.js +4 -0
  27. package/core/security/user.ts +20 -2
  28. package/core/server/REST.ts +29 -16
  29. package/core/server/Server.ts +6 -2
  30. package/core/server/http.ts +5 -7
  31. package/core/server/serverHelpers/Request.ts +5 -0
  32. package/core/server/serverHelpers/serverUtilities.ts +6 -2
  33. package/core/server/static.ts +0 -2
  34. package/core/unitTests/apiTests/cache-test.mjs +37 -0
  35. package/core/unitTests/commonTestErrors.js +4 -0
  36. package/core/unitTests/dataLayer/SQLSearch.test.js +2 -2
  37. package/core/unitTests/dataLayer/sql-update.test.js +2 -2
  38. package/core/unitTests/resources/auditLog.test.js +167 -0
  39. package/core/unitTests/resources/caching.test.js +79 -0
  40. package/core/unitTests/resources/permissions.test.js +7 -2
  41. package/core/unitTests/resources/txn-tracking.test.js +10 -4
  42. package/core/unitTests/resources/vectorIndex.test.js +1 -0
  43. package/core/unitTests/testApp/resources.js +30 -0
  44. package/core/unitTests/testApp/schema.graphql +5 -0
  45. package/core/unitTests/utility/operationPermissions.test.js +107 -0
  46. package/core/unitTests/utility/operation_authorization.test.js +130 -0
  47. package/core/unitTests/validation/role_validation.test.js +79 -0
  48. package/core/utility/common_utils.js +8 -4
  49. package/core/utility/errors/commonErrors.js +4 -0
  50. package/core/utility/hdbTerms.ts +1 -0
  51. package/core/utility/operationPermissions.ts +96 -0
  52. package/core/utility/operation_authorization.js +150 -49
  53. package/core/validation/role_validation.js +23 -0
  54. package/dist/bin/harper.js +1 -1
  55. package/dist/bin/harper.js.map +1 -1
  56. package/dist/cloneNode/cloneNode.js +55 -38
  57. package/dist/cloneNode/cloneNode.js.map +1 -1
  58. package/dist/core/bin/harper.js +2 -0
  59. package/dist/core/bin/harper.js.map +1 -1
  60. package/dist/core/components/ComponentV1.js +1 -0
  61. package/dist/core/components/ComponentV1.js.map +1 -1
  62. package/dist/core/components/EntryHandler.js +35 -1
  63. package/dist/core/components/EntryHandler.js.map +1 -1
  64. package/dist/core/components/PluginModule.js +1 -0
  65. package/dist/core/components/PluginModule.js.map +1 -1
  66. package/dist/core/components/Scope.js +2 -0
  67. package/dist/core/components/Scope.js.map +1 -1
  68. package/dist/core/components/componentLoader.js +12 -3
  69. package/dist/core/components/componentLoader.js.map +1 -1
  70. package/dist/core/components/status/api.js +1 -0
  71. package/dist/core/components/status/api.js.map +1 -1
  72. package/dist/core/components/status/crossThread.js +1 -0
  73. package/dist/core/components/status/crossThread.js.map +1 -1
  74. package/dist/core/config/RootConfigWatcher.js +34 -4
  75. package/dist/core/config/RootConfigWatcher.js.map +1 -1
  76. package/dist/core/dataLayer/harperBridge/ResourceBridge.js +1 -0
  77. package/dist/core/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  78. package/dist/core/resources/DatabaseTransaction.js +3 -1
  79. package/dist/core/resources/DatabaseTransaction.js.map +1 -1
  80. package/dist/core/resources/ErrorResource.js +1 -0
  81. package/dist/core/resources/ErrorResource.js.map +1 -1
  82. package/dist/core/resources/LMDBTransaction.js +10 -5
  83. package/dist/core/resources/LMDBTransaction.js.map +1 -1
  84. package/dist/core/resources/RecordEncoder.js +5 -3
  85. package/dist/core/resources/RecordEncoder.js.map +1 -1
  86. package/dist/core/resources/RequestTarget.js +3 -0
  87. package/dist/core/resources/RequestTarget.js.map +1 -1
  88. package/dist/core/resources/Resource.js +2 -1
  89. package/dist/core/resources/Resource.js.map +1 -1
  90. package/dist/core/resources/ResourceInterface.js +3 -0
  91. package/dist/core/resources/ResourceInterface.js.map +1 -1
  92. package/dist/core/resources/ResourceInterfaceV2.js +2 -0
  93. package/dist/core/resources/ResourceInterfaceV2.js.map +1 -1
  94. package/dist/core/resources/ResourceV2.js +3 -0
  95. package/dist/core/resources/ResourceV2.js.map +1 -1
  96. package/dist/core/resources/Resources.js +1 -0
  97. package/dist/core/resources/Resources.js.map +1 -1
  98. package/dist/core/resources/RocksIndexStore.js +1 -0
  99. package/dist/core/resources/RocksIndexStore.js.map +1 -1
  100. package/dist/core/resources/RocksTransactionLogStore.js +102 -12
  101. package/dist/core/resources/RocksTransactionLogStore.js.map +1 -1
  102. package/dist/core/resources/Table.js +100 -111
  103. package/dist/core/resources/Table.js.map +1 -1
  104. package/dist/core/resources/blob.js +1 -0
  105. package/dist/core/resources/blob.js.map +1 -1
  106. package/dist/core/resources/dataLoader.js +3 -2
  107. package/dist/core/resources/dataLoader.js.map +1 -1
  108. package/dist/core/resources/databases.js +1 -1
  109. package/dist/core/resources/databases.js.map +1 -1
  110. package/dist/core/resources/jsResource.js +2 -2
  111. package/dist/core/resources/jsResource.js.map +1 -1
  112. package/dist/core/resources/openApi.js +1 -0
  113. package/dist/core/resources/openApi.js.map +1 -1
  114. package/dist/core/resources/registrationDeprecated.js +11 -0
  115. package/dist/core/resources/registrationDeprecated.js.map +1 -0
  116. package/dist/core/resources/replayLogs.js +2 -0
  117. package/dist/core/resources/replayLogs.js.map +1 -1
  118. package/dist/core/resources/search.js +1 -0
  119. package/dist/core/resources/search.js.map +1 -1
  120. package/dist/core/resources/transactionBroadcast.js +30 -27
  121. package/dist/core/resources/transactionBroadcast.js.map +1 -1
  122. package/dist/core/security/auth.js +15 -1
  123. package/dist/core/security/auth.js.map +1 -1
  124. package/dist/core/security/jsLoader.js +10 -2
  125. package/dist/core/security/jsLoader.js.map +1 -1
  126. package/dist/core/security/permissionsTranslator.js +4 -0
  127. package/dist/core/security/permissionsTranslator.js.map +1 -1
  128. package/dist/core/security/user.js +16 -1
  129. package/dist/core/security/user.js.map +1 -1
  130. package/dist/core/server/REST.js +35 -18
  131. package/dist/core/server/REST.js.map +1 -1
  132. package/dist/core/server/Server.js +2 -0
  133. package/dist/core/server/Server.js.map +1 -1
  134. package/dist/core/server/http.js +5 -3
  135. package/dist/core/server/http.js.map +1 -1
  136. package/dist/core/server/operationsServer.js +2 -1
  137. package/dist/core/server/operationsServer.js.map +1 -1
  138. package/dist/core/server/serverHelpers/Request.js +2 -0
  139. package/dist/core/server/serverHelpers/Request.js.map +1 -1
  140. package/dist/core/server/serverHelpers/serverUtilities.js +3 -2
  141. package/dist/core/server/serverHelpers/serverUtilities.js.map +1 -1
  142. package/dist/core/server/static.js +1 -2
  143. package/dist/core/server/static.js.map +1 -1
  144. package/dist/core/utility/common_utils.js +7 -5
  145. package/dist/core/utility/common_utils.js.map +1 -1
  146. package/dist/core/utility/errors/commonErrors.js +3 -0
  147. package/dist/core/utility/errors/commonErrors.js.map +1 -1
  148. package/dist/core/utility/hdbTerms.js +1 -0
  149. package/dist/core/utility/hdbTerms.js.map +1 -1
  150. package/dist/core/utility/operationPermissions.js +101 -0
  151. package/dist/core/utility/operationPermissions.js.map +1 -0
  152. package/dist/core/utility/operation_authorization.js +83 -49
  153. package/dist/core/utility/operation_authorization.js.map +1 -1
  154. package/dist/core/validation/role_validation.js +19 -1
  155. package/dist/core/validation/role_validation.js.map +1 -1
  156. package/dist/licensing/usageLicensing.js +243 -0
  157. package/dist/licensing/usageLicensing.js.map +1 -0
  158. package/dist/licensing/validation.js +149 -0
  159. package/dist/licensing/validation.js.map +1 -0
  160. package/dist/replication/replicationConnection.js +90 -24
  161. package/dist/replication/replicationConnection.js.map +1 -1
  162. package/dist/replication/replicator.js +6 -2
  163. package/dist/replication/replicator.js.map +1 -1
  164. package/dist/replication/setNode.js +0 -1
  165. package/dist/replication/setNode.js.map +1 -1
  166. package/dist/security/certificate.js +206 -6
  167. package/dist/security/certificate.js.map +1 -1
  168. package/dist/security/keyService.js +58 -0
  169. package/dist/security/keyService.js.map +1 -0
  170. package/dist/security/sshKeyOperations.js +344 -0
  171. package/dist/security/sshKeyOperations.js.map +1 -0
  172. package/licensing/usageLicensing.ts +260 -0
  173. package/licensing/validation.ts +191 -0
  174. package/npm-shrinkwrap.json +509 -463
  175. package/package.json +6 -3
  176. package/replication/replicationConnection.ts +99 -31
  177. package/replication/replicator.ts +8 -3
  178. package/replication/setNode.ts +0 -1
  179. package/security/certificate.ts +259 -7
  180. package/security/keyService.ts +74 -0
  181. package/security/sshKeyOperations.ts +405 -0
  182. package/static/defaultConfig.yaml +2 -0
  183. package/studio/web/HDBDogOnly.svg +78 -0
  184. package/studio/web/assets/PPRadioGrotesk-Bold-DDaUYG8E.woff +0 -0
  185. package/studio/web/assets/fa-brands-400-CEJbCg16.woff +0 -0
  186. package/studio/web/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
  187. package/studio/web/assets/fa-brands-400-DnkPfk3o.eot +0 -0
  188. package/studio/web/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
  189. package/studio/web/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
  190. package/studio/web/assets/fa-regular-400-BhTwtT8w.eot +0 -0
  191. package/studio/web/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
  192. package/studio/web/assets/fa-regular-400-DFnMcJPd.woff +0 -0
  193. package/studio/web/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
  194. package/studio/web/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
  195. package/studio/web/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
  196. package/studio/web/assets/fa-solid-900-B6Axprfb.eot +0 -0
  197. package/studio/web/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
  198. package/studio/web/assets/fa-solid-900-DOXgCApm.woff +0 -0
  199. package/studio/web/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
  200. package/studio/web/assets/index-CjpELGtS.js +37 -0
  201. package/studio/web/assets/index-CjpELGtS.js.map +1 -0
  202. package/studio/web/assets/index-VoSl--bG.js +235 -0
  203. package/studio/web/assets/index-VoSl--bG.js.map +1 -0
  204. package/studio/web/assets/index-Y2g_iFpU.css +1 -0
  205. package/studio/web/assets/index-jiPwkrsB.css +1 -0
  206. package/studio/web/assets/index-o10iYrkU.js +2 -0
  207. package/studio/web/assets/index-o10iYrkU.js.map +1 -0
  208. package/studio/web/assets/index.lazy-D5hjT1pS.js +266 -0
  209. package/studio/web/assets/index.lazy-D5hjT1pS.js.map +1 -0
  210. package/studio/web/assets/profiler-B9Edexdi.js +2 -0
  211. package/studio/web/assets/profiler-B9Edexdi.js.map +1 -0
  212. package/studio/web/assets/react-redux-DsVOxD2E.js +6 -0
  213. package/studio/web/assets/react-redux-DsVOxD2E.js.map +1 -0
  214. package/studio/web/assets/startRecording-Bwd6AYRS.js +3 -0
  215. package/studio/web/assets/startRecording-Bwd6AYRS.js.map +1 -0
  216. package/studio/web/fabric-signup-background.webp +0 -0
  217. package/studio/web/fabric-signup-text.png +0 -0
  218. package/studio/web/favicon_purple.png +0 -0
  219. package/studio/web/github-icon.svg +15 -0
  220. package/studio/web/harper-fabric_black.png +0 -0
  221. package/studio/web/harper-fabric_white.png +0 -0
  222. package/studio/web/harper-studio_white.png +0 -0
  223. package/studio/web/index.html +16 -0
  224. package/studio/web/running.css +148 -0
  225. package/studio/web/running.html +147 -0
  226. package/studio/web/running.js +111 -0
@@ -127,9 +127,12 @@ function generateCommitsToPick(startCommit) {
127
127
  const commits = execSync(`git rev-list --reverse --first-parent ${startCommit}..old/main`)
128
128
  .toString()
129
129
  .trim()
130
- .split('\n');
131
- // write to file in case a human needs to take over
132
- fs.writeFileSync('commits-to-pick.txt', commits.join('\n') + '\n');
130
+ .split('\n')
131
+ .filter((c) => c !== '');
132
+ if (commits.length > 0) {
133
+ // write to file in case a human needs to take over
134
+ fs.writeFileSync('commits-to-pick.txt', commits.join('\n') + '\n');
135
+ }
133
136
  return commits;
134
137
  }
135
138
 
@@ -142,16 +145,24 @@ function isMergeCommit(commit) {
142
145
  return true;
143
146
  }
144
147
 
145
- function doItRockapella(startCommit) {
146
- process.stdout.write('Finding commits to sync... ');
147
- fetchCommits('old');
148
- pullRemoteBranch('origin', 'main');
148
+ function createSyncBranch() {
149
149
  const syncDate = new Date();
150
150
  const month = String(syncDate.getMonth() + 1).padStart(2, '0');
151
151
  const day = String(syncDate.getDate()).padStart(2, '0');
152
152
  checkoutNewBranch(`sync-${month}${day}${syncDate.getFullYear()}`);
153
+ }
154
+
155
+ function doItRockapella(startCommit) {
156
+ process.stdout.write('Finding commits to sync... ');
157
+ fetchCommits('old');
158
+ pullRemoteBranch('origin', 'main');
153
159
  const commits = generateCommitsToPick(startCommit);
154
160
  console.log('✅');
161
+ if (commits.length === 0) {
162
+ console.log('No commits to sync. Exiting.');
163
+ letsBail(0);
164
+ }
165
+ createSyncBranch();
155
166
  console.log(`\n${commits.length} commits found:`);
156
167
  for (const commit of commits) {
157
168
  if (isMergeCommit(commit)) {
@@ -0,0 +1,484 @@
1
+ /**
2
+ * operations RBAC integration tests.
3
+ *
4
+ * Tests the `operations` permission field on roles, which provides an
5
+ * operation-level allowlist enabling non-super_user roles to call specific
6
+ * operations (including SU-only ones) without full super_user access.
7
+ *
8
+ * Dual gate: operations restricts which ops are reachable, and table
9
+ * CRUD permissions still apply for data operations — both must pass.
10
+ */
11
+ import { suite, test, before, after } from 'node:test';
12
+ import { strictEqual, ok } from 'node:assert/strict';
13
+
14
+ import { setupHarper, teardownHarper, type ContextWithHarper } from '../utils/harperLifecycle.ts';
15
+
16
+ const DATABASE = 'test_db';
17
+ const TABLE = 'dogs';
18
+ const HASH_ATTR = 'id';
19
+
20
+ const READ_ONLY_ROLE = 'read_only_ops_role';
21
+ const READ_ONLY_USER = 'readonly_user';
22
+ const READ_ONLY_PASS = 'Test1234!';
23
+
24
+ const SU_OPS_ROLE = 'su_ops_role';
25
+ const SU_OPS_USER = 'su_ops_user';
26
+ const SU_OPS_PASS = 'Test1234!';
27
+
28
+ const COMBINED_ROLE = 'combined_ops_role';
29
+ const COMBINED_USER = 'combined_user';
30
+ const COMBINED_PASS = 'Test1234!';
31
+
32
+ const STANDARD_USER_ROLE = 'standard_user_ops_role';
33
+ const STANDARD_USER_USER = 'standard_user_user';
34
+ const STANDARD_USER_PASS = 'Test1234!';
35
+
36
+ suite('operations RBAC', (ctx: ContextWithHarper) => {
37
+ before(async () => {
38
+ await setupHarper(ctx, { config: {}, env: {} });
39
+
40
+ const adminAuth = `Basic ${Buffer.from(`${ctx.harper.admin.username}:${ctx.harper.admin.password}`).toString('base64')}`;
41
+
42
+ async function op(body: object) {
43
+ const res = await fetch(ctx.harper.operationsAPIURL, {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/json', 'Authorization': adminAuth },
46
+ body: JSON.stringify(body),
47
+ });
48
+ return res;
49
+ }
50
+
51
+ // Create database, table, and seed data
52
+ await op({ operation: 'create_database', database: DATABASE });
53
+ await op({ operation: 'create_table', schema: DATABASE, table: TABLE, hash_attribute: HASH_ATTR });
54
+ await op({
55
+ operation: 'insert',
56
+ schema: DATABASE,
57
+ table: TABLE,
58
+ records: [
59
+ { id: 1, name: 'Rex', breed: 'German Shepherd' },
60
+ { id: 2, name: 'Buddy', breed: 'Labrador' },
61
+ ],
62
+ });
63
+
64
+ // Create read_only_ops_role: operations restricts to read-only ops,
65
+ // with explicit READ permission on the test table (dual gate)
66
+ await op({
67
+ operation: 'add_role',
68
+ role: READ_ONLY_ROLE,
69
+ permission: {
70
+ operations: ['read_only'],
71
+ [DATABASE]: {
72
+ tables: {
73
+ [TABLE]: {
74
+ read: true,
75
+ insert: false,
76
+ update: false,
77
+ delete: false,
78
+ attribute_permissions: [],
79
+ },
80
+ },
81
+ },
82
+ },
83
+ });
84
+
85
+ // Create su_ops_role: can call specific SU-only ops without being super_user
86
+ await op({
87
+ operation: 'add_role',
88
+ role: SU_OPS_ROLE,
89
+ permission: {
90
+ operations: ['get_configuration', 'system_information', 'list_users'],
91
+ },
92
+ });
93
+
94
+ // Create combined_ops_role: both read_only data ops AND a specific SU-only op in one role
95
+ await op({
96
+ operation: 'add_role',
97
+ role: COMBINED_ROLE,
98
+ permission: {
99
+ operations: ['read_only', 'get_configuration'],
100
+ [DATABASE]: {
101
+ tables: {
102
+ [TABLE]: {
103
+ read: true,
104
+ insert: false,
105
+ update: false,
106
+ delete: false,
107
+ attribute_permissions: [],
108
+ },
109
+ },
110
+ },
111
+ },
112
+ });
113
+
114
+ // Create standard_user_ops_role: full CRUD data access + two SU-only ops,
115
+ // demonstrating the "all normally available ops + targeted admin ops" pattern
116
+ await op({
117
+ operation: 'add_role',
118
+ role: STANDARD_USER_ROLE,
119
+ permission: {
120
+ operations: ['standard_user', 'get_configuration', 'system_information'],
121
+ [DATABASE]: {
122
+ tables: {
123
+ [TABLE]: {
124
+ read: true,
125
+ insert: true,
126
+ update: true,
127
+ delete: true,
128
+ attribute_permissions: [],
129
+ },
130
+ },
131
+ },
132
+ },
133
+ });
134
+
135
+ // Create test users
136
+ await op({
137
+ operation: 'add_user',
138
+ role: READ_ONLY_ROLE,
139
+ username: READ_ONLY_USER,
140
+ password: READ_ONLY_PASS,
141
+ active: true,
142
+ });
143
+ await op({ operation: 'add_user', role: SU_OPS_ROLE, username: SU_OPS_USER, password: SU_OPS_PASS, active: true });
144
+ await op({
145
+ operation: 'add_user',
146
+ role: COMBINED_ROLE,
147
+ username: COMBINED_USER,
148
+ password: COMBINED_PASS,
149
+ active: true,
150
+ });
151
+ await op({
152
+ operation: 'add_user',
153
+ role: STANDARD_USER_ROLE,
154
+ username: STANDARD_USER_USER,
155
+ password: STANDARD_USER_PASS,
156
+ active: true,
157
+ });
158
+ });
159
+
160
+ after(async () => {
161
+ await teardownHarper(ctx);
162
+ });
163
+
164
+ // -- helpers --
165
+
166
+ function authHeader(username: string, password: string) {
167
+ return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
168
+ }
169
+
170
+ async function callOp(username: string, password: string, body: object) {
171
+ return fetch(ctx.harper.operationsAPIURL, {
172
+ method: 'POST',
173
+ headers: {
174
+ 'Content-Type': 'application/json',
175
+ 'Authorization': authHeader(username, password),
176
+ },
177
+ body: JSON.stringify(body),
178
+ });
179
+ }
180
+
181
+ // -- read_only_ops_role tests --
182
+
183
+ suite('read_only_ops_role', () => {
184
+ test('search_by_hash is allowed (in read_only group + table READ perm)', async () => {
185
+ const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
186
+ operation: 'search_by_hash',
187
+ schema: DATABASE,
188
+ table: TABLE,
189
+ hash_values: [1],
190
+ get_attributes: ['*'],
191
+ });
192
+ strictEqual(res.status, 200);
193
+ const body = (await res.json()) as any[];
194
+ ok(Array.isArray(body), 'Expected array response');
195
+ strictEqual(body[0].id, 1);
196
+ });
197
+
198
+ test('sql SELECT is allowed (in read_only group + table READ perm)', async () => {
199
+ const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
200
+ operation: 'sql',
201
+ sql: `SELECT * FROM ${DATABASE}.${TABLE} WHERE id = 1`,
202
+ });
203
+ strictEqual(res.status, 200);
204
+ });
205
+
206
+ test('describe_all is allowed (in read_only group)', async () => {
207
+ const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
208
+ operation: 'describe_all',
209
+ });
210
+ strictEqual(res.status, 200);
211
+ });
212
+
213
+ test('insert is denied (not in operations list)', async () => {
214
+ const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
215
+ operation: 'insert',
216
+ schema: DATABASE,
217
+ table: TABLE,
218
+ records: [{ id: 99, name: 'Max', breed: 'Poodle' }],
219
+ });
220
+ strictEqual(res.status, 403);
221
+ const body = (await res.json()) as any;
222
+ ok(
223
+ body.unauthorized_access?.[0]?.includes("'insert' is not permitted for this role's operations configuration"),
224
+ `Unexpected denial reason: ${JSON.stringify(body.unauthorized_access)}`
225
+ );
226
+ });
227
+
228
+ test('update is denied (not in operations list)', async () => {
229
+ const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
230
+ operation: 'update',
231
+ schema: DATABASE,
232
+ table: TABLE,
233
+ records: [{ id: 1, name: 'Rex Updated' }],
234
+ });
235
+ strictEqual(res.status, 403);
236
+ });
237
+
238
+ test('delete is denied (not in operations list)', async () => {
239
+ const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
240
+ operation: 'delete',
241
+ schema: DATABASE,
242
+ table: TABLE,
243
+ hash_values: [1],
244
+ });
245
+ strictEqual(res.status, 403);
246
+ });
247
+
248
+ test('get_configuration is denied (SU-only op not in operations list)', async () => {
249
+ const res = await callOp(READ_ONLY_USER, READ_ONLY_PASS, {
250
+ operation: 'get_configuration',
251
+ });
252
+ strictEqual(res.status, 403);
253
+ });
254
+ });
255
+
256
+ // -- su_ops_role tests --
257
+
258
+ suite('su_ops_role', () => {
259
+ test('get_configuration is allowed (SU-only op granted via operations)', async () => {
260
+ const res = await callOp(SU_OPS_USER, SU_OPS_PASS, {
261
+ operation: 'get_configuration',
262
+ });
263
+ strictEqual(res.status, 200);
264
+ });
265
+
266
+ test('system_information is allowed (SU-only op granted via operations)', async () => {
267
+ const res = await callOp(SU_OPS_USER, SU_OPS_PASS, {
268
+ operation: 'system_information',
269
+ });
270
+ strictEqual(res.status, 200);
271
+ });
272
+
273
+ test('list_users is allowed (SU-only op granted via operations)', async () => {
274
+ const res = await callOp(SU_OPS_USER, SU_OPS_PASS, {
275
+ operation: 'list_users',
276
+ });
277
+ strictEqual(res.status, 200);
278
+ });
279
+
280
+ test('insert is denied (not in operations list)', async () => {
281
+ const res = await callOp(SU_OPS_USER, SU_OPS_PASS, {
282
+ operation: 'insert',
283
+ schema: DATABASE,
284
+ table: TABLE,
285
+ records: [{ id: 99, name: 'Max', breed: 'Poodle' }],
286
+ });
287
+ strictEqual(res.status, 403);
288
+ });
289
+
290
+ test('restart is denied (SU-only op not in operations list)', async () => {
291
+ const res = await callOp(SU_OPS_USER, SU_OPS_PASS, {
292
+ operation: 'restart',
293
+ });
294
+ strictEqual(res.status, 403);
295
+ });
296
+
297
+ test('search_by_hash is denied (not in operations list)', async () => {
298
+ const res = await callOp(SU_OPS_USER, SU_OPS_PASS, {
299
+ operation: 'search_by_hash',
300
+ schema: DATABASE,
301
+ table: TABLE,
302
+ hash_values: [1],
303
+ get_attributes: ['*'],
304
+ });
305
+ strictEqual(res.status, 403);
306
+ });
307
+ });
308
+
309
+ // -- role validation tests --
310
+
311
+ suite('add_role validation', () => {
312
+ test('non-array operations is rejected with 400', async () => {
313
+ const res = await callOp(ctx.harper.admin.username, ctx.harper.admin.password, {
314
+ operation: 'add_role',
315
+ role: 'bad_role_1',
316
+ permission: {
317
+ operations: true,
318
+ },
319
+ });
320
+ strictEqual(res.status, 400);
321
+ const body = (await res.json()) as any;
322
+ ok(JSON.stringify(body).includes('must be an array'), `Unexpected response: ${JSON.stringify(body)}`);
323
+ });
324
+
325
+ test('invalid operation name in operations is rejected with 400', async () => {
326
+ const res = await callOp(ctx.harper.admin.username, ctx.harper.admin.password, {
327
+ operation: 'add_role',
328
+ role: 'bad_role_2',
329
+ permission: {
330
+ operations: ['bogus_nonexistent_op'],
331
+ },
332
+ });
333
+ strictEqual(res.status, 400);
334
+ const body = (await res.json()) as any;
335
+ ok(JSON.stringify(body).includes('bogus_nonexistent_op'), `Unexpected response: ${JSON.stringify(body)}`);
336
+ });
337
+
338
+ test('valid operations with read_only group is accepted', async () => {
339
+ const res = await callOp(ctx.harper.admin.username, ctx.harper.admin.password, {
340
+ operation: 'add_role',
341
+ role: 'valid_role_1',
342
+ permission: {
343
+ operations: ['read_only'],
344
+ },
345
+ });
346
+ strictEqual(res.status, 200);
347
+ });
348
+ });
349
+
350
+ // -- combined role: both data ops (read_only group) and SU-only op granted together --
351
+
352
+ suite('combined_ops_role (read_only + SU-only op)', () => {
353
+ test('search_by_hash is allowed (data op via read_only group + table READ perm)', async () => {
354
+ const res = await callOp(COMBINED_USER, COMBINED_PASS, {
355
+ operation: 'search_by_hash',
356
+ schema: DATABASE,
357
+ table: TABLE,
358
+ hash_values: [1],
359
+ get_attributes: ['*'],
360
+ });
361
+ strictEqual(res.status, 200);
362
+ });
363
+
364
+ test('get_configuration is allowed (SU-only bypass via operations)', async () => {
365
+ const res = await callOp(COMBINED_USER, COMBINED_PASS, {
366
+ operation: 'get_configuration',
367
+ });
368
+ strictEqual(res.status, 200);
369
+ });
370
+
371
+ test('insert is denied (not in operations list despite table existing in perms)', async () => {
372
+ const res = await callOp(COMBINED_USER, COMBINED_PASS, {
373
+ operation: 'insert',
374
+ schema: DATABASE,
375
+ table: TABLE,
376
+ records: [{ id: 98, name: 'Daisy', breed: 'Corgi' }],
377
+ });
378
+ strictEqual(res.status, 403);
379
+ });
380
+
381
+ test('restart is denied (SU-only op not in operations list)', async () => {
382
+ const res = await callOp(COMBINED_USER, COMBINED_PASS, {
383
+ operation: 'restart',
384
+ });
385
+ strictEqual(res.status, 403);
386
+ });
387
+ });
388
+
389
+ // -- standard_user group: all non-SU data ops + targeted SU ops --
390
+ // This mirrors the docs example: "all normally available access + two SU operations"
391
+
392
+ suite('standard_user_ops_role (standard_user group + targeted SU ops)', () => {
393
+ test('search_by_hash is allowed (in standard_user group + table READ perm)', async () => {
394
+ const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
395
+ operation: 'search_by_hash',
396
+ schema: DATABASE,
397
+ table: TABLE,
398
+ hash_values: [1],
399
+ get_attributes: ['*'],
400
+ });
401
+ strictEqual(res.status, 200);
402
+ });
403
+
404
+ test('insert is allowed (in standard_user group + table INSERT perm)', async () => {
405
+ const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
406
+ operation: 'insert',
407
+ schema: DATABASE,
408
+ table: TABLE,
409
+ records: [{ id: 10, name: 'Luna', breed: 'Husky' }],
410
+ });
411
+ strictEqual(res.status, 200);
412
+ });
413
+
414
+ test('update is allowed (in standard_user group + table UPDATE perm)', async () => {
415
+ const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
416
+ operation: 'update',
417
+ schema: DATABASE,
418
+ table: TABLE,
419
+ records: [{ id: 10, name: 'Luna Updated' }],
420
+ });
421
+ strictEqual(res.status, 200);
422
+ });
423
+
424
+ test('delete is allowed (in standard_user group + table DELETE perm)', async () => {
425
+ const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
426
+ operation: 'delete',
427
+ schema: DATABASE,
428
+ table: TABLE,
429
+ hash_values: [10],
430
+ });
431
+ strictEqual(res.status, 200);
432
+ });
433
+
434
+ test('get_configuration is allowed (SU-only op explicitly granted)', async () => {
435
+ const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
436
+ operation: 'get_configuration',
437
+ });
438
+ strictEqual(res.status, 200);
439
+ });
440
+
441
+ test('system_information is allowed (SU-only op explicitly granted)', async () => {
442
+ const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
443
+ operation: 'system_information',
444
+ });
445
+ strictEqual(res.status, 200);
446
+ });
447
+
448
+ test('restart is denied (SU-only op not in operations list)', async () => {
449
+ const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
450
+ operation: 'restart',
451
+ });
452
+ strictEqual(res.status, 403);
453
+ });
454
+
455
+ test('drop_database is denied (SU-only op not in operations list)', async () => {
456
+ const res = await callOp(STANDARD_USER_USER, STANDARD_USER_PASS, {
457
+ operation: 'drop_database',
458
+ database: DATABASE,
459
+ });
460
+ strictEqual(res.status, 403);
461
+ });
462
+ });
463
+
464
+ // -- regression: admin super_user behavior unchanged --
465
+
466
+ suite('admin (super_user) regression', () => {
467
+ test('admin can insert (no operations restriction)', async () => {
468
+ const res = await callOp(ctx.harper.admin.username, ctx.harper.admin.password, {
469
+ operation: 'insert',
470
+ schema: DATABASE,
471
+ table: TABLE,
472
+ records: [{ id: 50, name: 'Bella', breed: 'Beagle' }],
473
+ });
474
+ strictEqual(res.status, 200);
475
+ });
476
+
477
+ test('admin can get_configuration (super_user unrestricted)', async () => {
478
+ const res = await callOp(ctx.harper.admin.username, ctx.harper.admin.password, {
479
+ operation: 'get_configuration',
480
+ });
481
+ strictEqual(res.status, 200);
482
+ });
483
+ });
484
+ });
package/core/package.json CHANGED
@@ -32,6 +32,7 @@
32
32
  "scripts": {
33
33
  "build": "tsc --project tsconfig.build.json",
34
34
  "build:watch": "npm run build -- --watch --incremental",
35
+ "package": "npm run build; npm shrinkwrap && npm pack",
35
36
  "lint": "oxlint --deny-warnings .",
36
37
  "lint:required": "oxlint --quiet .",
37
38
  "lint:fix": "npm run lint -- --fix",
@@ -41,8 +42,8 @@
41
42
  "test:integration": "node integrationTests/utils/scripts/run.ts",
42
43
  "test:unit": "mocha",
43
44
  "test:unit:main": "mocha 'unitTests/**/*test.*js' --exclude 'unitTests/apiTests/**/*' --exclude 'unitTests/dataLayer/harperBridge/**/*' --exclude 'unitTests/resources/**/*'",
44
- "test:unit:all": "npm run test:unit:main && npm run test:unit:apitests && npm run test:unit:resources",
45
- "test:unit:lmdb": "HARPER_STORAGE_ENGINE=lmdb npm run test:unit:apitests && npm run test:unit:resources",
45
+ "test:unit:all": "npm run test:unit:main && npm run test:unit:apitests && npm run test:unit:resources && npm run test:unit:lmdb",
46
+ "test:unit:lmdb": "HARPER_STORAGE_ENGINE=lmdb npm run test:unit:resources && HARPER_STORAGE_ENGINE=lmdb npm run test:unit:apitests",
46
47
  "test:unit:components": "mocha 'unitTests/components/**/*.js'",
47
48
  "test:unit:resources": "mocha 'unitTests/resources/**/*.js'",
48
49
  "test:unit:bin": "mocha 'unitTests/bin/**/*.js'",
@@ -139,7 +140,7 @@
139
140
  "@fastify/cors": "~9.0.1",
140
141
  "@fastify/static": "~7.0.4",
141
142
  "@harperfast/extended-iterable": "^1.0.1",
142
- "@harperfast/rocksdb-js": "^0.1.7",
143
+ "@harperfast/rocksdb-js": "^0.1.10",
143
144
  "@turf/area": "6.5.0",
144
145
  "@turf/boolean-contains": "6.5.0",
145
146
  "@turf/boolean-disjoint": "6.5.0",
@@ -213,13 +213,14 @@ export class DatabaseTransaction implements Transaction {
213
213
  if (!outstandingCommit) {
214
214
  outstandingCommit = commitResolution;
215
215
  outstandingCommitStart = performance.now();
216
- outstandingCommit.then(() => {
216
+ outstandingCommit.finally(() => {
217
217
  outstandingCommit = null;
218
218
  });
219
219
  }
220
220
  const completions = [];
221
221
  return commitResolution.then(
222
222
  () => {
223
+ this.transaction.onCommit?.();
223
224
  this.transaction = null; // the native transaction is done (reset if needed)
224
225
  if (this.next) {
225
226
  completions.push(this.next.commit(options));
@@ -42,6 +42,7 @@ export class LMDBTransaction extends DatabaseTransaction {
42
42
  getReadTxn(): ReadTransaction {
43
43
  // used optimistically
44
44
  this.readTxnRefCount = (this.readTxnRefCount || 0) + 1;
45
+ this.timeout = txnExpiration; // reset the timeout
45
46
  if (this.stale) this.stale = false;
46
47
  if (this.readTxn) {
47
48
  if (this.readTxn.openTimer) this.readTxn.openTimer = 0;
@@ -321,15 +322,19 @@ let timer;
321
322
  function startMonitoringTxns() {
322
323
  timer = setInterval(function () {
323
324
  for (const txn of trackedTxns) {
324
- if (txn.stale) {
325
+ if (txn.timeout <= 0) {
325
326
  const url = txn.getContext()?.url;
326
327
  harperLogger.error(
327
- `Transaction was open too long and has been aborted, from table: ${
328
+ `Transaction was open too long and has been committed, from table: ${
328
329
  txn.db?.name + (url ? ' path: ' + url : '')
329
330
  }`
330
331
  );
331
- txn.abort();
332
- } else txn.stale = true;
332
+ // reset the transaction
333
+ txn.commit();
334
+ txn.timeout = txnExpiration;
335
+ } else {
336
+ txn.timeout -= txnExpiration;
337
+ }
333
338
  }
334
339
  }, txnExpiration).unref();
335
340
  }
@@ -558,6 +558,7 @@ export function recordUpdater(store, tableId, auditStore) {
558
558
  store.encoder.structureUpdate = null;
559
559
  }
560
560
  const structureVersion = store.encoder.structures.length + (store.encoder.typedStructs?.length ?? 0);
561
+ const nodeId = options?.nodeId ?? server.replication?.getThisNodeId(auditStore) ?? 0;
561
562
  if (resolveRecord && existingEntry?.localTime) {
562
563
  const replacingId = existingEntry?.localTime;
563
564
  const replacingEntry = auditStore.get(replacingId, tableId, id);
@@ -570,7 +571,7 @@ export function recordUpdater(store, tableId, auditStore) {
570
571
  tableId,
571
572
  recordId: id,
572
573
  previousVersion,
573
- nodeId: options?.nodeId ?? server.replication.getThisNodeId(auditStore) ?? 0,
574
+ nodeId,
574
575
  user: username,
575
576
  type,
576
577
  encodedRecord: lastValueEncoding,
@@ -580,7 +581,7 @@ export function recordUpdater(store, tableId, auditStore) {
580
581
  expiresAt,
581
582
  structureVersion,
582
583
  },
583
- { ifVersion: ifVersion, transaction: options.transaction }
584
+ { ifVersion: ifVersion, transaction: options.transaction, nodeId }
584
585
  );
585
586
  return result;
586
587
  }
@@ -592,7 +593,7 @@ export function recordUpdater(store, tableId, auditStore) {
592
593
  tableId,
593
594
  recordId: id,
594
595
  previousVersion: store instanceof RocksDatabase ? existingEntry?.version : existingEntry?.localTime ? 1 : 0,
595
- nodeId: options?.nodeId ?? server.replication?.getThisNodeId(auditStore) ?? 0,
596
+ nodeId,
596
597
  user: username,
597
598
  type,
598
599
  encodedRecord: lastValueEncoding,
@@ -609,6 +610,7 @@ export function recordUpdater(store, tableId, auditStore) {
609
610
  instructedWrite: true,
610
611
  ifVersion,
611
612
  transaction: options.transaction,
613
+ nodeId,
612
614
  }
613
615
  );
614
616
  }
@@ -29,7 +29,7 @@ export class RequestTarget extends URLSearchParams {
29
29
  declare operator?: 'AND' | 'OR';
30
30
  /** The sort attribute and direction to use */
31
31
  /** @ts-expect-error USP has a sort method, we hide it */
32
- declare sort?: Sort = null;
32
+ sort?: Sort = null;
33
33
  /** The selected attributes to return */
34
34
  declare select?: Select;
35
35
  /** Return an explanation of the query order */
@@ -191,7 +191,7 @@ export class Resource<Record extends object = any> implements ResourceInterface<
191
191
  }
192
192
  static invalidate = transactional(
193
193
  function (resource: Resource, query: RequestTarget, _request: Context, _data: any) {
194
- return resource.invalidate ? resource.invalidate(query) : missingMethod(resource, 'delete');
194
+ return resource.invalidate ? resource.invalidate(query) : missingMethod(resource, 'invalidate');
195
195
  },
196
196
  { hasContent: false, type: 'update', method: 'invalidate' }
197
197
  );