@harperfast/harper 5.2.0-alpha.4 → 5.2.0-alpha.6

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 (173) hide show
  1. package/agent/agent.ts +152 -10
  2. package/agent/bestPractices.ts +58 -0
  3. package/agent/mcpTools.ts +122 -0
  4. package/agent/operations.ts +10 -1
  5. package/agent/registryTools.ts +117 -0
  6. package/agent/session.ts +16 -7
  7. package/agent/tools/fsTools.ts +74 -27
  8. package/agent/tools/inspectorTool.ts +459 -0
  9. package/agent/toolset.ts +27 -7
  10. package/agent/types.ts +2 -0
  11. package/components/Application.ts +566 -85
  12. package/components/OptionsWatcher.ts +62 -2
  13. package/components/Scope.ts +31 -8
  14. package/components/componentLoader.ts +5 -1
  15. package/components/componentSecrets.ts +514 -55
  16. package/components/deploymentRecorder.ts +7 -6
  17. package/components/gitCredentialHelper.js +115 -0
  18. package/components/gitCredentialServer.ts +251 -0
  19. package/components/mcp/tools/application.ts +197 -23
  20. package/components/mcp/tools/operations.ts +1 -1
  21. package/components/mcp/tools/schemas/derive.ts +6 -2
  22. package/components/operations.js +21 -19
  23. package/components/operationsValidation.js +86 -21
  24. package/components/secretOperations.ts +110 -30
  25. package/config/harperConfigEnvVars.ts +90 -5
  26. package/config-root.schema.json +4 -0
  27. package/dist/agent/agent.d.ts +24 -3
  28. package/dist/agent/agent.js +172 -10
  29. package/dist/agent/agent.js.map +1 -1
  30. package/dist/agent/bestPractices.d.ts +24 -0
  31. package/dist/agent/bestPractices.js +60 -0
  32. package/dist/agent/bestPractices.js.map +1 -0
  33. package/dist/agent/mcpTools.d.ts +24 -0
  34. package/dist/agent/mcpTools.js +115 -0
  35. package/dist/agent/mcpTools.js.map +1 -0
  36. package/dist/agent/operations.js +10 -1
  37. package/dist/agent/operations.js.map +1 -1
  38. package/dist/agent/registryTools.d.ts +45 -0
  39. package/dist/agent/registryTools.js +113 -0
  40. package/dist/agent/registryTools.js.map +1 -0
  41. package/dist/agent/session.js +16 -7
  42. package/dist/agent/session.js.map +1 -1
  43. package/dist/agent/tools/fsTools.js +70 -28
  44. package/dist/agent/tools/fsTools.js.map +1 -1
  45. package/dist/agent/tools/inspectorTool.d.ts +40 -0
  46. package/dist/agent/tools/inspectorTool.js +428 -0
  47. package/dist/agent/tools/inspectorTool.js.map +1 -0
  48. package/dist/agent/toolset.d.ts +16 -6
  49. package/dist/agent/toolset.js +17 -7
  50. package/dist/agent/toolset.js.map +1 -1
  51. package/dist/agent/types.d.ts +2 -0
  52. package/dist/components/Application.d.ts +73 -18
  53. package/dist/components/Application.js +475 -64
  54. package/dist/components/Application.js.map +1 -1
  55. package/dist/components/OptionsWatcher.d.ts +1 -1
  56. package/dist/components/OptionsWatcher.js +65 -2
  57. package/dist/components/OptionsWatcher.js.map +1 -1
  58. package/dist/components/Scope.d.ts +8 -6
  59. package/dist/components/Scope.js +22 -6
  60. package/dist/components/Scope.js.map +1 -1
  61. package/dist/components/componentLoader.js +5 -1
  62. package/dist/components/componentLoader.js.map +1 -1
  63. package/dist/components/componentSecrets.d.ts +33 -7
  64. package/dist/components/componentSecrets.js +473 -59
  65. package/dist/components/componentSecrets.js.map +1 -1
  66. package/dist/components/deploymentRecorder.d.ts +2 -2
  67. package/dist/components/deploymentRecorder.js +1 -1
  68. package/dist/components/deploymentRecorder.js.map +1 -1
  69. package/dist/components/gitCredentialHelper.d.ts +1 -0
  70. package/dist/components/gitCredentialHelper.js +113 -0
  71. package/dist/components/gitCredentialHelper.js.map +1 -0
  72. package/dist/components/gitCredentialServer.d.ts +33 -0
  73. package/dist/components/gitCredentialServer.js +236 -0
  74. package/dist/components/gitCredentialServer.js.map +1 -0
  75. package/dist/components/mcp/tools/application.d.ts +19 -0
  76. package/dist/components/mcp/tools/application.js +180 -22
  77. package/dist/components/mcp/tools/application.js.map +1 -1
  78. package/dist/components/mcp/tools/operations.d.ts +13 -0
  79. package/dist/components/mcp/tools/operations.js +1 -0
  80. package/dist/components/mcp/tools/operations.js.map +1 -1
  81. package/dist/components/mcp/tools/schemas/derive.js +6 -2
  82. package/dist/components/mcp/tools/schemas/derive.js.map +1 -1
  83. package/dist/components/operations.js +23 -21
  84. package/dist/components/operations.js.map +1 -1
  85. package/dist/components/operationsValidation.js +84 -21
  86. package/dist/components/operationsValidation.js.map +1 -1
  87. package/dist/components/secretOperations.d.ts +33 -11
  88. package/dist/components/secretOperations.js +90 -26
  89. package/dist/components/secretOperations.js.map +1 -1
  90. package/dist/config/harperConfigEnvVars.d.ts +21 -0
  91. package/dist/config/harperConfigEnvVars.js +95 -5
  92. package/dist/config/harperConfigEnvVars.js.map +1 -1
  93. package/dist/index.d.ts +4 -0
  94. package/dist/index.js +14 -1
  95. package/dist/index.js.map +1 -1
  96. package/dist/resources/Resource.d.ts +19 -0
  97. package/dist/resources/Resource.js +93 -2
  98. package/dist/resources/Resource.js.map +1 -1
  99. package/dist/resources/Table.d.ts +9 -0
  100. package/dist/resources/Table.js +256 -50
  101. package/dist/resources/Table.js.map +1 -1
  102. package/dist/resources/analytics/read.js +28 -25
  103. package/dist/resources/analytics/read.js.map +1 -1
  104. package/dist/resources/defineResource.d.ts +180 -0
  105. package/dist/resources/defineResource.js +505 -0
  106. package/dist/resources/defineResource.js.map +1 -0
  107. package/dist/resources/defineTable.d.ts +221 -0
  108. package/dist/resources/defineTable.js +227 -0
  109. package/dist/resources/defineTable.js.map +1 -0
  110. package/dist/resources/jsonSchemaTypes.d.ts +2 -0
  111. package/dist/resources/jsonSchemaTypes.js +12 -4
  112. package/dist/resources/jsonSchemaTypes.js.map +1 -1
  113. package/dist/resources/openApi.js +69 -13
  114. package/dist/resources/openApi.js.map +1 -1
  115. package/dist/resources/search.js +6 -8
  116. package/dist/resources/search.js.map +1 -1
  117. package/dist/server/graphqlQuerying.js +4 -2
  118. package/dist/server/graphqlQuerying.js.map +1 -1
  119. package/dist/server/http.d.ts +12 -0
  120. package/dist/server/http.js +35 -15
  121. package/dist/server/http.js.map +1 -1
  122. package/dist/server/serverHelpers/serverUtilities.js +8 -5
  123. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  124. package/dist/server/serverHelpers/uwsServer.js +23 -0
  125. package/dist/server/serverHelpers/uwsServer.js.map +1 -1
  126. package/dist/utility/errors/hdbError.d.ts +21 -0
  127. package/dist/utility/errors/hdbError.js +23 -1
  128. package/dist/utility/errors/hdbError.js.map +1 -1
  129. package/dist/validation/configValidator.js +22 -6
  130. package/dist/validation/configValidator.js.map +1 -1
  131. package/index.ts +33 -0
  132. package/npm-shrinkwrap.json +9197 -15236
  133. package/package.json +5 -3
  134. package/resources/DESIGN.md +42 -15
  135. package/resources/Resource.ts +99 -2
  136. package/resources/Table.ts +275 -71
  137. package/resources/analytics/read.ts +30 -25
  138. package/resources/defineResource.ts +651 -0
  139. package/resources/defineTable.ts +407 -0
  140. package/resources/jsonSchemaTypes.ts +12 -4
  141. package/resources/openApi.ts +68 -16
  142. package/resources/search.ts +5 -8
  143. package/server/graphqlQuerying.ts +4 -2
  144. package/server/http.ts +40 -16
  145. package/server/serverHelpers/serverUtilities.ts +19 -6
  146. package/server/serverHelpers/uwsServer.ts +24 -0
  147. package/studio/web/assets/{Chat-D4FIaBu7.js → Chat-CTjtL8Z4.js} +2 -2
  148. package/studio/web/assets/{Chat-D4FIaBu7.js.map → Chat-CTjtL8Z4.js.map} +1 -1
  149. package/studio/web/assets/{FloatingChat-DGUC3L4r.js → FloatingChat-CafHR4Ur.js} +4 -4
  150. package/studio/web/assets/{FloatingChat-DGUC3L4r.js.map → FloatingChat-CafHR4Ur.js.map} +1 -1
  151. package/studio/web/assets/{applications-D8Am9ikM.js → applications-Buh_q0Vj.js} +2 -2
  152. package/studio/web/assets/{applications-D8Am9ikM.js.map → applications-Buh_q0Vj.js.map} +1 -1
  153. package/studio/web/assets/{index-HGL4WHdb.js → index-0hXeECkS.js} +6 -6
  154. package/studio/web/assets/{index-HGL4WHdb.js.map → index-0hXeECkS.js.map} +1 -1
  155. package/studio/web/assets/{index.lazy-Dk5j1AoQ.js → index.lazy-B00B7VBT.js} +4 -4
  156. package/studio/web/assets/{index.lazy-Dk5j1AoQ.js.map → index.lazy-B00B7VBT.js.map} +1 -1
  157. package/studio/web/assets/{profile-7bu_CF1f.js → profile-Cg2wwYPn.js} +2 -2
  158. package/studio/web/assets/{profile-7bu_CF1f.js.map → profile-Cg2wwYPn.js.map} +1 -1
  159. package/studio/web/assets/{setComponentFile-MfYm9F94.js → setComponentFile-DCaDIvyB.js} +2 -2
  160. package/studio/web/assets/{setComponentFile-MfYm9F94.js.map → setComponentFile-DCaDIvyB.js.map} +1 -1
  161. package/studio/web/assets/{setup-SKA4UhdE.js → setup-CAVcAQjK.js} +2 -2
  162. package/studio/web/assets/{setup-SKA4UhdE.js.map → setup-CAVcAQjK.js.map} +1 -1
  163. package/studio/web/assets/{status-DetVLUxR.js → status-BRXorNdD.js} +2 -2
  164. package/studio/web/assets/{status-DetVLUxR.js.map → status-BRXorNdD.js.map} +1 -1
  165. package/studio/web/assets/{swagger-ui-react-DLeYARY2.js → swagger-ui-react-Dy1D62vO.js} +2 -2
  166. package/studio/web/assets/{swagger-ui-react-DLeYARY2.js.map → swagger-ui-react-Dy1D62vO.js.map} +1 -1
  167. package/studio/web/assets/{tsMode-DGO-jIF7.js → tsMode-A8gbL74v.js} +2 -2
  168. package/studio/web/assets/{tsMode-DGO-jIF7.js.map → tsMode-A8gbL74v.js.map} +1 -1
  169. package/studio/web/assets/{useEntityRestURL-4M2i6bo4.js → useEntityRestURL-cDodrVcQ.js} +2 -2
  170. package/studio/web/assets/{useEntityRestURL-4M2i6bo4.js.map → useEntityRestURL-cDodrVcQ.js.map} +1 -1
  171. package/studio/web/index.html +1 -1
  172. package/utility/errors/hdbError.ts +32 -0
  173. package/validation/configValidator.ts +23 -6
@@ -43,6 +43,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
43
43
  Object.defineProperty(exports, "__esModule", { value: true });
44
44
  exports.EVICTED = exports.INVALIDATED = void 0;
45
45
  exports.freezeRecord = freezeRecord;
46
+ exports.frozenRecordView = frozenRecordView;
46
47
  exports.makeTable = makeTable;
47
48
  exports.coerceType = coerceType;
48
49
  const hdbTerms_ts_1 = require("../utility/hdbTerms.js");
@@ -133,6 +134,45 @@ function freezeRecord(value) {
133
134
  if (value !== null && typeof value === 'object' && !ArrayBuffer.isView(value) && !(value instanceof ArrayBuffer))
134
135
  Object.freeze(value);
135
136
  }
137
+ // Returns a read-only VIEW of `record` for evaluation (e.g. a record-scoped allowRead guard)
138
+ // without mutating the original. Locally-loaded records are already frozen by loadLocalRecord, so
139
+ // this is a no-op there; the source-revalidation path hands back the SAME object its deferred
140
+ // commit still writes to (createdAt/updatedAt) and persists, so that object can't be frozen
141
+ // directly. A write-blocking Proxy is used instead of a shallow copy: a copy would eagerly invoke
142
+ // every getter (breaking a caching table's lazy structPrototype decode) and silently drop
143
+ // non-enumerable properties like Array.length. Only ordinary objects/arrays are wrapped — Date,
144
+ // Map, Set, RegExp, etc. carry internal slots that throw "incompatible receiver" when their
145
+ // methods run against anything but the exact original instance (a Proxy included), so those are
146
+ // returned unwrapped rather than risk breaking an override that calls a method on one; a cached
147
+ // record CAN legitimately be one of these (getFromSource only requires `typeof === 'object'`).
148
+ function frozenRecordView(record) {
149
+ if (record === null || typeof record !== 'object' || ArrayBuffer.isView(record) || record instanceof ArrayBuffer)
150
+ return record;
151
+ if (Object.isFrozen(record))
152
+ return record;
153
+ const tag = Object.prototype.toString.call(record);
154
+ if (tag !== '[object Object]' && tag !== '[object Array]')
155
+ return record;
156
+ return new Proxy(record, {
157
+ get(target, prop) {
158
+ // receiver = target (not this proxy) so a lazy-decode getter runs with `this` bound to
159
+ // the real record, matching its behavior when read directly.
160
+ return Reflect.get(target, prop, target);
161
+ },
162
+ set() {
163
+ return false;
164
+ },
165
+ defineProperty() {
166
+ return false;
167
+ },
168
+ deleteProperty() {
169
+ return false;
170
+ },
171
+ setPrototypeOf() {
172
+ return false;
173
+ },
174
+ });
175
+ }
136
176
  exports.INVALIDATED = 1;
137
177
  exports.EVICTED = 8; // note that 2 is reserved for timestamps
138
178
  const TEST_WRITE_KEY_BUFFER = Buffer.allocUnsafeSlow(8192);
@@ -144,6 +184,12 @@ const REPLAY_YIELD_INTERVAL = 100; // yield to the event loop every N records du
144
184
  // buffer the entire backward chain per record, synchronously, on every worker — pinning the JS heap
145
185
  // until the worker OOMs (issue #1114). Beyond this depth we fall back to a bounded reconciliation.
146
186
  const MAX_OUT_OF_ORDER_AUDIT_DEPTH = 1000;
187
+ // Cap on audit records inspected while backfilling a subscription's `previousCount` history,
188
+ // independent of `count` (the number of AUTHORIZED events collected). `count` only decrements on
189
+ // records that pass the row-level allowRead filter, so an all-deny override would otherwise force
190
+ // a full walk of the retained audit log (#1786) — this bounds that walk regardless of how many
191
+ // records the filter accepts.
192
+ const MAX_PREVIOUS_COUNT_SCAN = 10_000;
147
193
  const FULL_PERMISSIONS = {
148
194
  read: true,
149
195
  insert: true,
@@ -2459,18 +2505,44 @@ function makeTable(options) {
2459
2505
  throw new Error('No query provided');
2460
2506
  if (target.parseError)
2461
2507
  throw target.parseError; // if there was a parse error, we can throw it now
2508
+ // An application-overridden allowRead is a RECORD-scoped check (#1422 gap 2 / #1241): it is
2509
+ // evaluated once per record during query execution with `this` = the record, instead of once
2510
+ // at entry where `this` is a collection resource with nothing loaded. The framework defaults
2511
+ // (marked isDefaultAllowRead) are this-free table/RBAC checks and keep the entry evaluation.
2512
+ // Record-scoping is SYNC-only: async-declared overrides keep entry-check semantics (the
2513
+ // authorize wrapper awaits them and fails closed) rather than entering the sync traversal.
2514
+ const recordScopedAllowRead = target.checkPermission &&
2515
+ !this.allowRead?.isDefaultAllowRead &&
2516
+ this.allowRead?.constructor?.name !== 'AsyncFunction'
2517
+ ? this.allowRead
2518
+ : undefined;
2462
2519
  if (target.checkPermission) {
2463
- // requesting authorization verification
2464
- let allowed;
2465
- try {
2466
- allowed = this.allowRead(context.user, target, context);
2467
- }
2468
- catch {
2469
- // allow* threw — fail closed rather than letting the request proceed
2470
- throw new hdbError_ts_1.AccessViolation(context.user);
2520
+ if (recordScopedAllowRead) {
2521
+ // Compose with role-level column RBAC: the DEFAULT table allowRead is also the
2522
+ // enforcement point for attribute_permissions (it narrows target.select). Run it here
2523
+ // for that side effect — before `select` is captured below — with its verdict
2524
+ // superseded by the per-record override. A per-record super.allowRead call could NOT
2525
+ // restore this: the select-driven output transform is built before iteration starts.
2526
+ try {
2527
+ TableResource.prototype.allowRead.call(this, context.user, target, context);
2528
+ }
2529
+ catch {
2530
+ // narrowing is best-effort under an override; access is governed per record
2531
+ }
2471
2532
  }
2472
- if (!allowed) {
2473
- throw new hdbError_ts_1.AccessViolation(context.user);
2533
+ else {
2534
+ // requesting authorization verification
2535
+ let allowed;
2536
+ try {
2537
+ allowed = this.allowRead(context.user, target, context);
2538
+ }
2539
+ catch {
2540
+ // allow* threw — fail closed rather than letting the request proceed
2541
+ throw new hdbError_ts_1.AccessViolation(context.user);
2542
+ }
2543
+ if (!allowed) {
2544
+ throw new hdbError_ts_1.AccessViolation(context.user);
2545
+ }
2474
2546
  }
2475
2547
  }
2476
2548
  if (context)
@@ -2672,27 +2744,54 @@ function makeTable(options) {
2672
2744
  // scans), the read transaction reads against the latest committed data without pinning a
2673
2745
  // consistent snapshot, so the scan doesn't hold a snapshot that blocks compaction.
2674
2746
  const readTxn = txn.useReadTxn(target.snapshot === false);
2675
- // Record-level read guard (#1241): a resource may define a static allowReadRecord(user, record)
2676
- // that participates in query execution (pushed into HNSW traversal for vector sorts, applied as a
2677
- // post-filter otherwise). Resolve off the actual (possibly subclassed) constructor so overrides win.
2678
- // SCOPE: this is a QUERY-result filter, not a general read-authorization boundary direct
2679
- // single-record get(id) does not consult it (use allowRead/instance hooks for that). It must be
2680
- // synchronous, side-effect free, and fast; it can run once per candidate record during traversal.
2681
- const allowReadRecord = this.constructor.allowReadRecord;
2682
- const recordAccess = typeof allowReadRecord === 'function' || typeof target.vectorFilter === 'function'
2683
- ? { allowReadRecord, user: context?.user, vectorFilter: target.vectorFilter }
2747
+ // Record-level allowRead guard (#1241/#1422): an application-overridden allowRead runs once
2748
+ // per record with `this` = the (frozen) record, participating in query execution pushed
2749
+ // into HNSW traversal for vector sorts, applied as a post-filter otherwise. It must be
2750
+ // synchronous, side-effect free, and fast; a throw denies that record (fail closed, #1422
2751
+ // gap 1 parity). Dispatched via the method resolved from the resource never via a
2752
+ // `record.allowRead` property lookup, so a record attribute named allowRead can't shadow it.
2753
+ let recordGuard;
2754
+ if (recordScopedAllowRead) {
2755
+ const user = context?.user;
2756
+ let warnedAsync = false;
2757
+ recordGuard = (record) => {
2758
+ let allowed;
2759
+ try {
2760
+ allowed = recordScopedAllowRead.call(record, user, target, context);
2761
+ }
2762
+ catch {
2763
+ return false; // fail closed on a throwing check
2764
+ }
2765
+ if (typeof allowed?.then === 'function') {
2766
+ // A sync-declared override that returns a thenable can't be awaited mid-traversal:
2767
+ // deny the record (fail closed, #1422 gap 1 semantics) rather than fail open on
2768
+ // promise truthiness or abort the whole query. Declared-async overrides never get
2769
+ // here — they keep the awaited entry check. We're not observing this rejection, so
2770
+ // attach a no-op handler — otherwise a rejected thenable reaches the global
2771
+ // unhandledRejection logger once per denied candidate. Use `.then(undefined, ...)`,
2772
+ // not `.catch` — a thenable is only guaranteed to have `.then`.
2773
+ allowed.then(undefined, () => { });
2774
+ if (!warnedAsync) {
2775
+ warnedAsync = true;
2776
+ logger_ts_1.logger.warn?.(`allowRead on ${tableName} returned a promise during per-record evaluation; records are denied (record-scoped allowRead must be synchronous)`);
2777
+ }
2778
+ return false;
2779
+ }
2780
+ return Boolean(allowed);
2781
+ };
2782
+ }
2783
+ const recordAccess = recordGuard || typeof target.vectorFilter === 'function'
2784
+ ? { recordGuard, vectorFilter: target.vectorFilter }
2684
2785
  : undefined;
2685
2786
  const entries = (0, search_ts_1.executeConditions)(conditions, operator, TableResource, readTxn, target, context, (results, filters) => transformToEntries(results, select, context, readTxn, filters), filtered, recordAccess);
2686
2787
  const ensure_loaded = target.ensureLoaded !== false;
2687
- // Authoritative RBAC enforcement (#1241): the guards inside executeConditions evaluate the LOCAL
2688
- // record, but on a caching table transformEntryForSelect may then revalidate an expired/invalidated
2689
- // row from source and return a DIFFERENT record. An authorization check must hold on the record
2690
- // actually returned, so allowReadRecord is re-checked there, after materialization (the earlier
2691
- // evaluation stays as a prune that also bounds HNSW traversal). vectorFilter and condition filters
2692
- // intentionally keep the local-record semantics all query filters have on caching tables.
2693
- const recordGuard = typeof allowReadRecord === 'function'
2694
- ? (record) => allowReadRecord(context?.user, record)
2695
- : undefined;
2788
+ // Authoritative enforcement point (#1241): the guards inside executeConditions evaluate the
2789
+ // LOCAL record, but on a caching table transformEntryForSelect may then revalidate an
2790
+ // expired/invalidated row from source and return a DIFFERENT record. An authorization check
2791
+ // must hold on the record actually returned, so the record-scoped allowRead is re-checked
2792
+ // there, after materialization (the earlier evaluation stays as a prune that also bounds HNSW
2793
+ // traversal). vectorFilter and condition filters intentionally keep the local-record
2794
+ // semantics all query filters have on caching tables.
2696
2795
  const transformToRecord = TableResource.transformEntryForSelect(select, context, readTxn, filtered, ensure_loaded, true, recordGuard);
2697
2796
  let results = TableResource.transformToOrderedSelect(entries, select, postOrdering, context, readTxn, transformToRecord);
2698
2797
  // apply any offset/limit after all the sorting and filtering
@@ -2989,8 +3088,10 @@ function makeTable(options) {
2989
3088
  return canSkip ? extended_iterable_1.SKIP : record;
2990
3089
  // Record-level RBAC (#1241): enforced here because `record` is now the final, materialized
2991
3090
  // record — a caching table's source revalidation (above) may have replaced the local copy the
2992
- // query filters evaluated. Fail closed on the record actually being returned.
2993
- if (recordGuard && !recordGuard(record))
3091
+ // query filters evaluated. Fail closed on the record actually being returned. The guard sees a
3092
+ // frozen view (frozenRecordView) so an allowRead override that writes through `this` can't
3093
+ // mutate a record the source-revalidation path's deferred commit still needs to write and encode.
3094
+ if (recordGuard && !recordGuard(frozenRecordView(record)))
2994
3095
  return canSkip ? extended_iterable_1.SKIP : undefined;
2995
3096
  if (select && !(select[0] === '*' && select.length === 1)) {
2996
3097
  let promises;
@@ -3162,6 +3263,58 @@ function makeTable(options) {
3162
3263
  let reloadResnapshotRunning = false;
3163
3264
  let reloadResnapshotPending = false;
3164
3265
  const thisId = requestTargetToId(request) ?? null; // treat undefined and null as the root
3266
+ // Record-scoped allowRead on subscription delivery (#1419, reviving #1524 on the unified
3267
+ // model): the subscribe entry check already granted the connection (topic ACL / collection
3268
+ // scope); here we ADDITIONALLY re-evaluate a sync application-overridden allowRead per
3269
+ // record-bearing event with `this` = the event's record, the delivery-path analog of the
3270
+ // per-record query guard. Gated to (1) an overridden allowRead (the default is
3271
+ // record-independent and already enforced at connect, so the common case stays zero-overhead)
3272
+ // and (2) an authorization-checked subscription — the entry check clears checkPermission
3273
+ // before we get here and an anonymous-but-checked subscription has no user to key on, so the
3274
+ // wrapper stamps a durable `rowLevelAuthChecked` flag; internal subscribers (replication,
3275
+ // system watchers) never set it and keep full delivery.
3276
+ // Fails closed: a throwing or thenable-returning override drops the event rather than leaks it.
3277
+ //
3278
+ // Known limitation (as in #1524): only put/invalidate events carry the authoritative full
3279
+ // record. delete (tombstone, null value), message payloads, and rawEvents partial values may
3280
+ // mis-decide an override keyed on row fields; closing the primary leak (record updates) is
3281
+ // the goal here — authorizing non-record-bearing event types against the full row is deferred.
3282
+ const subContext = this.getContext();
3283
+ const subAllowRead = this.allowRead;
3284
+ const filterRowReads = request?.rowLevelAuthChecked && !subAllowRead?.isDefaultAllowRead;
3285
+ let warnedAsyncEvent = false;
3286
+ const allowsEvent = filterRowReads
3287
+ ? (event) => {
3288
+ if (event.type === 'end_txn' || event.type === 'reload')
3289
+ return true; // control markers, no record
3290
+ // Freeze the record before the override sees it, mirroring the query path — event.value
3291
+ // is the shared primaryStore object, so an override that writes to `this` would otherwise
3292
+ // mutate the cache for every reader on the node.
3293
+ if (event.value)
3294
+ freezeRecord(event.value);
3295
+ let decision;
3296
+ try {
3297
+ // Evaluate against the LIVE context user, not a subscribe-time snapshot: #1414 re-auth
3298
+ // updates subContext.user in place, so a role/claims change must be reflected per event.
3299
+ decision = subAllowRead.call(event.value ?? null, subContext.user, request, subContext);
3300
+ }
3301
+ catch {
3302
+ return false; // fail closed: a throwing override drops the event
3303
+ }
3304
+ if (decision != null && typeof decision.then === 'function') {
3305
+ // Not observing this rejection — attach a no-op handler so it doesn't reach the
3306
+ // global unhandledRejection logger once per dropped event. Use `.then(undefined,
3307
+ // ...)`, not `.catch` — a thenable is only guaranteed to have `.then`.
3308
+ decision.then(undefined, () => { });
3309
+ if (!warnedAsyncEvent) {
3310
+ warnedAsyncEvent = true;
3311
+ logger_ts_1.logger.warn?.(`allowRead on ${tableName} returned a promise during subscription event evaluation; events are dropped (record-scoped allowRead must be synchronous)`);
3312
+ }
3313
+ return false;
3314
+ }
3315
+ return Boolean(decision);
3316
+ }
3317
+ : null;
3165
3318
  const subscription = (0, transactionBroadcast_ts_1.addSubscription)(TableResource, thisId, function (id, auditRecord, localTime, beginTxn) {
3166
3319
  if (dropDuringReplay)
3167
3320
  return;
@@ -3206,9 +3359,13 @@ function makeTable(options) {
3206
3359
  type,
3207
3360
  beginTxn,
3208
3361
  };
3362
+ // Queued events are filtered when the queue drains through send() below; events sent
3363
+ // directly (queue already drained) are filtered here. Each event is filtered once.
3209
3364
  if (pendingRealTimeQueue)
3210
3365
  pendingRealTimeQueue.push(event);
3211
3366
  else {
3367
+ if (allowsEvent && !allowsEvent(event))
3368
+ return;
3212
3369
  if (databaseName !== 'system') {
3213
3370
  (0, write_ts_1.recordAction)(auditRecord.size ?? 1, 'db-message', tableName, null);
3214
3371
  }
@@ -3287,25 +3444,45 @@ function makeTable(options) {
3287
3444
  }
3288
3445
  else if (count) {
3289
3446
  const history = [];
3447
+ let inspected = 0;
3290
3448
  // we are collecting the history in reverse order to get the right count, then reversing to send
3291
3449
  for (const auditRecord of auditStore.getRange({ start: 'z', end: false, reverse: true })) {
3292
3450
  if (++recordsSinceYield >= REPLAY_YIELD_INTERVAL) {
3293
3451
  recordsSinceYield = 0;
3294
3452
  await rest();
3453
+ // Subscription.end() nulls `subscriptions` on close; stop backfilling a dead
3454
+ // subscriber rather than continuing to walk the retained audit log.
3455
+ if (!subscription.subscriptions)
3456
+ break;
3295
3457
  }
3296
3458
  try {
3297
3459
  if (auditRecord.tableId !== tableId)
3298
3460
  continue;
3299
3461
  const id = auditRecord.recordId;
3300
3462
  if (thisId == null || isDescendantId(thisId, id)) {
3463
+ // Bound entries INSPECTED for THIS scope, independent of `count` (entries
3464
+ // AUTHORIZED) — an all-deny allowRead override must not force a full walk of
3465
+ // the retained audit log (#1786), even though it returns fewer than `count`
3466
+ // authorized events. Counted only once a record is known to be in scope (right
3467
+ // table, right id) so unrelated cross-table/cross-scope audit traffic in a busy
3468
+ // shared log can't spuriously cut a backfill short.
3469
+ if (++inspected > MAX_PREVIOUS_COUNT_SCAN) {
3470
+ logger_ts_1.logger.warn?.(`previousCount backfill on ${tableName} stopped after inspecting ${MAX_PREVIOUS_COUNT_SCAN} in-scope audit records without collecting ${request.previousCount} authorized event(s); returning ${history.length} instead`);
3471
+ break;
3472
+ }
3301
3473
  const value = auditRecord.getValue(primaryStore, getFullRecord, auditRecord.localTime);
3302
- history.push({
3474
+ const historyEntry = {
3303
3475
  id,
3304
3476
  localTime: auditRecord.localTime,
3305
3477
  value,
3306
3478
  version: auditRecord.version,
3307
3479
  type: auditRecord.type,
3308
- });
3480
+ };
3481
+ // Filter denied rows BEFORE they consume a previousCount slot, so an authorized
3482
+ // subscriber still receives up to `count` readable events (#1419).
3483
+ if (allowsEvent && !allowsEvent(historyEntry))
3484
+ continue;
3485
+ history.push(historyEntry);
3309
3486
  if (--count <= 0)
3310
3487
  break;
3311
3488
  }
@@ -3458,6 +3635,9 @@ function makeTable(options) {
3458
3635
  subscription.send(error);
3459
3636
  });
3460
3637
  function send(event) {
3638
+ // Covers the pendingRealTimeQueue drain and the reload re-snapshot (#495) delivery.
3639
+ if (allowsEvent && !allowsEvent(event))
3640
+ return;
3461
3641
  if (databaseName !== 'system') {
3462
3642
  (0, write_ts_1.recordAction)(event.size ?? 1, 'db-message', tableName, null);
3463
3643
  }
@@ -3663,21 +3843,27 @@ function makeTable(options) {
3663
3843
  }
3664
3844
  // #section: validation
3665
3845
  validate(record, patch) {
3846
+ // Accumulate structured per-field issues so the 400 carries `{ path, code,
3847
+ // message }[]` matching the emitted OpenAPI, instead of a single joined string. The joined
3848
+ // message is still built for the HTTP title, preserving back-compat for callers that read it.
3666
3849
  let validationErrors;
3850
+ const addError = (path, code, message) => {
3851
+ (validationErrors || (validationErrors = [])).push({ path, code, message });
3852
+ };
3667
3853
  const validateValue = (value, attribute, name) => {
3668
3854
  if (attribute.type && value != null) {
3669
3855
  if (patch && value.__op__)
3670
3856
  value = value.value;
3671
3857
  if (attribute.properties) {
3672
3858
  if (typeof value !== 'object') {
3673
- (validationErrors || (validationErrors = [])).push(`Value ${stringify(value)} in property ${name} must be an object${attribute.type ? ' (' + attribute.type + ')' : ''}`);
3859
+ addError(name, 'type', `Value ${stringify(value)} in property ${name} must be an object${attribute.type ? ' (' + attribute.type + ')' : ''}`);
3674
3860
  }
3675
3861
  const properties = attribute.properties;
3676
3862
  for (let i = 0, l = properties.length; i < l; i++) {
3677
3863
  const attribute = properties[i];
3678
3864
  if (attribute.relationship || attribute.computed) {
3679
3865
  if (record.hasOwnProperty(attribute.name)) {
3680
- (validationErrors || (validationErrors = [])).push(`Computed property ${name}.${attribute.name} may not be directly assigned a value`);
3866
+ addError(`${name}.${attribute.name}`, 'computed', `Computed property ${name}.${attribute.name} may not be directly assigned a value`);
3681
3867
  }
3682
3868
  continue;
3683
3869
  }
@@ -3688,7 +3874,7 @@ function makeTable(options) {
3688
3874
  if (attribute.sealed && value != null && typeof value === 'object') {
3689
3875
  for (const key in value) {
3690
3876
  if (!properties.find((property) => property.name === key)) {
3691
- (validationErrors || (validationErrors = [])).push(`Property ${key} is not allowed within object in property ${name}`);
3877
+ addError(`${name}.${key}`, 'unknown_property', `Property ${key} is not allowed within object in property ${name}`);
3692
3878
  }
3693
3879
  }
3694
3880
  }
@@ -3697,35 +3883,35 @@ function makeTable(options) {
3697
3883
  switch (attribute.type) {
3698
3884
  case 'Int':
3699
3885
  if (typeof value !== 'number' || value >> 0 !== value)
3700
- (validationErrors || (validationErrors = [])).push(`Value ${stringify(value)} in property ${name} must be an integer (from -2147483648 to 2147483647)`);
3886
+ addError(name, 'type', `Value ${stringify(value)} in property ${name} must be an integer (from -2147483648 to 2147483647)`);
3701
3887
  break;
3702
3888
  case 'Long':
3703
3889
  if (typeof value !== 'number' || !(Math.floor(value) === value && Math.abs(value) <= 9007199254740992))
3704
- (validationErrors || (validationErrors = [])).push(`Value ${stringify(value)} in property ${name} must be an integer (from -9007199254740992 to 9007199254740992)`);
3890
+ addError(name, 'type', `Value ${stringify(value)} in property ${name} must be an integer (from -9007199254740992 to 9007199254740992)`);
3705
3891
  break;
3706
3892
  case 'Float':
3707
3893
  if (typeof value !== 'number')
3708
- (validationErrors || (validationErrors = [])).push(`Value ${stringify(value)} in property ${name} must be a number`);
3894
+ addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a number`);
3709
3895
  break;
3710
3896
  case 'ID':
3711
3897
  if (!(typeof value === 'string' ||
3712
3898
  (value?.length > 0 && value.every?.((value) => typeof value === 'string'))))
3713
- (validationErrors || (validationErrors = [])).push(`Value ${stringify(value)} in property ${name} must be a string, or an array of strings`);
3899
+ addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a string, or an array of strings`);
3714
3900
  break;
3715
3901
  case 'String':
3716
3902
  if (typeof value !== 'string')
3717
- (validationErrors || (validationErrors = [])).push(`Value ${stringify(value)} in property ${name} must be a string`);
3903
+ addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a string`);
3718
3904
  break;
3719
3905
  case 'Boolean':
3720
3906
  if (typeof value !== 'boolean')
3721
- (validationErrors || (validationErrors = [])).push(`Value ${stringify(value)} in property ${name} must be a boolean`);
3907
+ addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a boolean`);
3722
3908
  break;
3723
3909
  case 'Date':
3724
3910
  if (!(value instanceof Date)) {
3725
3911
  if (typeof value === 'string' || typeof value === 'number')
3726
3912
  return new Date(value);
3727
3913
  else
3728
- (validationErrors || (validationErrors = [])).push(`Value ${stringify(value)} in property ${name} must be a Date`);
3914
+ addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a Date`);
3729
3915
  }
3730
3916
  break;
3731
3917
  case 'BigInt':
@@ -3733,14 +3919,14 @@ function makeTable(options) {
3733
3919
  // do coercion because otherwise it is rather difficult to get numbers to consistently be bigints
3734
3920
  if (typeof value === 'string' || typeof value === 'number')
3735
3921
  return BigInt(value);
3736
- (validationErrors || (validationErrors = [])).push(`Value ${stringify(value)} in property ${name} must be a bigint`);
3922
+ addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a bigint`);
3737
3923
  }
3738
3924
  break;
3739
3925
  case 'Bytes':
3740
3926
  if (!(value instanceof Uint8Array)) {
3741
3927
  if (typeof value === 'string')
3742
3928
  return Buffer.from(value);
3743
- (validationErrors || (validationErrors = [])).push(`Value ${stringify(value)} in property ${name} must be a Buffer or Uint8Array`);
3929
+ addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a Buffer or Uint8Array`);
3744
3930
  }
3745
3931
  break;
3746
3932
  case 'Blob':
@@ -3750,7 +3936,7 @@ function makeTable(options) {
3750
3936
  if (value instanceof Buffer) {
3751
3937
  return createBlob(value, { type: 'text/plain' });
3752
3938
  }
3753
- (validationErrors || (validationErrors = [])).push(`Value ${stringify(value)} in property ${name} must be a Blob`);
3939
+ addError(name, 'type', `Value ${stringify(value)} in property ${name} must be a Blob`);
3754
3940
  }
3755
3941
  break;
3756
3942
  case 'array':
@@ -3765,20 +3951,20 @@ function makeTable(options) {
3765
3951
  }
3766
3952
  }
3767
3953
  else
3768
- (validationErrors || (validationErrors = [])).push(`Value ${stringify(value)} in property ${name} must be an Array`);
3954
+ addError(name, 'type', `Value ${stringify(value)} in property ${name} must be an Array`);
3769
3955
  break;
3770
3956
  }
3771
3957
  }
3772
3958
  }
3773
3959
  if (attribute.nullable === false && value == null) {
3774
- (validationErrors || (validationErrors = [])).push(`Property ${name} is required (and not does not allow null values)`);
3960
+ addError(name, 'required', `Property ${name} is required (and not does not allow null values)`);
3775
3961
  }
3776
3962
  };
3777
3963
  for (let i = 0, l = attributes.length; i < l; i++) {
3778
3964
  const attribute = attributes[i];
3779
3965
  if (attribute.relationship || attribute.computed) {
3780
3966
  if (Object.hasOwn(record, attribute.name)) {
3781
- (validationErrors || (validationErrors = [])).push(`Computed property ${attribute.name} may not be directly assigned a value`);
3967
+ addError(attribute.name, 'computed', `Computed property ${attribute.name} may not be directly assigned a value`);
3782
3968
  }
3783
3969
  continue;
3784
3970
  }
@@ -3791,12 +3977,12 @@ function makeTable(options) {
3791
3977
  if (sealed) {
3792
3978
  for (const key in record) {
3793
3979
  if (!attributes.find((attribute) => attribute.name === key)) {
3794
- (validationErrors || (validationErrors = [])).push(`Property ${key} is not allowed`);
3980
+ addError(key, 'unknown_property', `Property ${key} is not allowed`);
3795
3981
  }
3796
3982
  }
3797
3983
  }
3798
3984
  if (validationErrors) {
3799
- throw new hdbError_ts_1.ClientError(validationErrors.join('. '));
3985
+ throw new hdbError_ts_1.ValidationError(validationErrors, validationErrors.map((issue) => issue.message).join('. '));
3800
3986
  }
3801
3987
  }
3802
3988
  // #section: stats-admin
@@ -4284,6 +4470,26 @@ function makeTable(options) {
4284
4470
  }, () => {
4285
4471
  throw new hdbError_ts_1.ServerError('Service unavailable, exceeded request queue limit for resolving cache record', 503);
4286
4472
  });
4473
+ // Mark the table-level allowRead as a framework default (it is a this-free table/RBAC check):
4474
+ // only an APPLICATION override is record-scoped and evaluated per record during query execution
4475
+ // (#1422 gap 2 / #1241). supportsRowLevelAllowRead tells the authorization wrapper in
4476
+ // Resource.ts that this class has the per-record machinery to defer collection reads to.
4477
+ TableResource.prototype.allowRead.isDefaultAllowRead = true;
4478
+ TableResource.supportsRowLevelAllowRead = true;
4479
+ // Records can be asked directly (`record.allowRead(user, target, context)`), delegating to the
4480
+ // table's allowRead with `this` = the record. Non-enumerable so it never serializes; defined on
4481
+ // the per-table record prototype alongside computed properties. Engine-level enforcement does
4482
+ // NOT route through this (it resolves the active resource class's allowRead, which honors
4483
+ // endpoint subclass overrides and can't be shadowed by a record attribute of the same name).
4484
+ if (primaryStore?.encoder?.structPrototype) {
4485
+ Object.defineProperty(primaryStore.encoder.structPrototype, 'allowRead', {
4486
+ value: function (user, target, context) {
4487
+ return TableResource.prototype.allowRead.call(this, user, target, context);
4488
+ },
4489
+ configurable: true,
4490
+ writable: true,
4491
+ });
4492
+ }
4287
4493
  TableResource.updatedAttributes(); // on creation, update accessors as well
4288
4494
  if (expirationMs)
4289
4495
  TableResource.setTTLExpiration(expirationMs / 1000);