@rocicorp/zero 1.11.3 → 1.11.5

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 (192) hide show
  1. package/out/replicache/src/sync/request-id.js +2 -2
  2. package/out/replicache/src/sync/request-id.js.map +1 -1
  3. package/out/shared/src/bigint-json.d.ts.map +1 -1
  4. package/out/shared/src/bigint-json.js +1 -0
  5. package/out/shared/src/bigint-json.js.map +1 -1
  6. package/out/shared/src/headers.d.ts +12 -0
  7. package/out/shared/src/headers.d.ts.map +1 -0
  8. package/out/shared/src/headers.js +20 -0
  9. package/out/shared/src/headers.js.map +1 -0
  10. package/out/shared/src/random-uint64.d.ts.map +1 -1
  11. package/out/shared/src/random-uint64.js +2 -2
  12. package/out/shared/src/random-uint64.js.map +1 -1
  13. package/out/shared/src/random-values.d.ts +14 -1
  14. package/out/shared/src/random-values.d.ts.map +1 -1
  15. package/out/shared/src/random-values.js +24 -4
  16. package/out/shared/src/random-values.js.map +1 -1
  17. package/out/zero/package.js +1 -1
  18. package/out/zero/package.js.map +1 -1
  19. package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
  20. package/out/zero-cache/src/auth/auth.js +5 -0
  21. package/out/zero-cache/src/auth/auth.js.map +1 -1
  22. package/out/zero-cache/src/config/zero-config.d.ts +21 -0
  23. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  24. package/out/zero-cache/src/config/zero-config.js +47 -9
  25. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  26. package/out/zero-cache/src/custom-queries/transform-query.js +2 -2
  27. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  28. package/out/zero-cache/src/db/pg-to-lite.d.ts.map +1 -1
  29. package/out/zero-cache/src/db/pg-to-lite.js +3 -2
  30. package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
  31. package/out/zero-cache/src/db/pg-type-parser.d.ts +13 -0
  32. package/out/zero-cache/src/db/pg-type-parser.d.ts.map +1 -1
  33. package/out/zero-cache/src/db/pg-type-parser.js +55 -2
  34. package/out/zero-cache/src/db/pg-type-parser.js.map +1 -1
  35. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  36. package/out/zero-cache/src/server/change-streamer.js +5 -2
  37. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  38. package/out/zero-cache/src/server/inspector-delegate.d.ts +2 -25
  39. package/out/zero-cache/src/server/inspector-delegate.d.ts.map +1 -1
  40. package/out/zero-cache/src/server/inspector-delegate.js +6 -29
  41. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  42. package/out/zero-cache/src/server/main.d.ts.map +1 -1
  43. package/out/zero-cache/src/server/main.js +5 -0
  44. package/out/zero-cache/src/server/main.js.map +1 -1
  45. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  46. package/out/zero-cache/src/server/replicator.js +3 -2
  47. package/out/zero-cache/src/server/replicator.js.map +1 -1
  48. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  49. package/out/zero-cache/src/server/syncer.js +5 -4
  50. package/out/zero-cache/src/server/syncer.js.map +1 -1
  51. package/out/zero-cache/src/server/worker-dispatcher.d.ts.map +1 -1
  52. package/out/zero-cache/src/server/worker-dispatcher.js +2 -1
  53. package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
  54. package/out/zero-cache/src/services/admin-auth.d.ts +22 -0
  55. package/out/zero-cache/src/services/admin-auth.d.ts.map +1 -0
  56. package/out/zero-cache/src/services/admin-auth.js +42 -0
  57. package/out/zero-cache/src/services/admin-auth.js.map +1 -0
  58. package/out/zero-cache/src/services/auth-attempt-counter.d.ts +33 -0
  59. package/out/zero-cache/src/services/auth-attempt-counter.d.ts.map +1 -0
  60. package/out/zero-cache/src/services/auth-attempt-counter.js +61 -0
  61. package/out/zero-cache/src/services/auth-attempt-counter.js.map +1 -0
  62. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +1 -1
  63. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +7 -1
  64. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  65. package/out/zero-cache/src/services/change-source/pg/change-source.js +110 -13
  66. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  67. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +1 -1
  68. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +1 -1
  69. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +15 -8
  70. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
  71. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +38 -5
  72. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  73. package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
  74. package/out/zero-cache/src/services/change-source/pg/schema/init.js +10 -1
  75. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  76. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +4 -0
  77. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
  78. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +10 -3
  79. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  80. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +7 -1
  81. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  82. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +23 -2
  83. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  84. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +2 -2
  85. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  86. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +30 -9
  87. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  88. package/out/zero-cache/src/services/change-streamer/change-streamer.js +10 -3
  89. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  90. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts +1 -0
  91. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts.map +1 -1
  92. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +1 -1
  93. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
  94. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  95. package/out/zero-cache/src/services/heapz.d.ts.map +1 -1
  96. package/out/zero-cache/src/services/heapz.js +4 -5
  97. package/out/zero-cache/src/services/heapz.js.map +1 -1
  98. package/out/zero-cache/src/services/mutagen/pusher.js +1 -1
  99. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  100. package/out/zero-cache/src/services/plannerz.d.ts.map +1 -1
  101. package/out/zero-cache/src/services/plannerz.js +3 -7
  102. package/out/zero-cache/src/services/plannerz.js.map +1 -1
  103. package/out/zero-cache/src/services/profz.d.ts.map +1 -1
  104. package/out/zero-cache/src/services/profz.js +4 -13
  105. package/out/zero-cache/src/services/profz.js.map +1 -1
  106. package/out/zero-cache/src/services/replicator/change-processor.js +1 -2
  107. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  108. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  109. package/out/zero-cache/src/services/replicator/incremental-sync.js +3 -3
  110. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  111. package/out/zero-cache/src/services/statz.d.ts.map +1 -1
  112. package/out/zero-cache/src/services/statz.js +2 -7
  113. package/out/zero-cache/src/services/statz.js.map +1 -1
  114. package/out/zero-cache/src/services/view-syncer/client-handler.d.ts +16 -0
  115. package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
  116. package/out/zero-cache/src/services/view-syncer/client-handler.js +22 -1
  117. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  118. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +29 -4
  119. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.map +1 -1
  120. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +40 -2
  121. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  122. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts +10 -2
  123. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
  124. package/out/zero-cache/src/services/view-syncer/cvr-store.js +54 -17
  125. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  126. package/out/zero-cache/src/services/view-syncer/cvr.d.ts +8 -0
  127. package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
  128. package/out/zero-cache/src/services/view-syncer/cvr.js +11 -0
  129. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  130. package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.d.ts +35 -0
  131. package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.d.ts.map +1 -0
  132. package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js +107 -0
  133. package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js.map +1 -0
  134. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts +2 -2
  135. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts.map +1 -1
  136. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +11 -6
  137. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  138. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  139. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +2 -1
  140. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  141. package/out/zero-cache/src/services/view-syncer/schema/cvr.d.ts +9 -0
  142. package/out/zero-cache/src/services/view-syncer/schema/cvr.d.ts.map +1 -1
  143. package/out/zero-cache/src/services/view-syncer/schema/cvr.js +4 -1
  144. package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
  145. package/out/zero-cache/src/services/view-syncer/schema/init.d.ts.map +1 -1
  146. package/out/zero-cache/src/services/view-syncer/schema/init.js +3 -0
  147. package/out/zero-cache/src/services/view-syncer/schema/init.js.map +1 -1
  148. package/out/zero-cache/src/services/view-syncer/snapshotter.js +1 -1
  149. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  150. package/out/zero-cache/src/services/view-syncer/view-syncer.js +96 -44
  151. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  152. package/out/zero-cache/src/types/client-group-log-id.d.ts +9 -0
  153. package/out/zero-cache/src/types/client-group-log-id.d.ts.map +1 -0
  154. package/out/zero-cache/src/types/client-group-log-id.js +16 -0
  155. package/out/zero-cache/src/types/client-group-log-id.js.map +1 -0
  156. package/out/zero-cache/src/types/lite.d.ts.map +1 -1
  157. package/out/zero-cache/src/types/lite.js +1 -0
  158. package/out/zero-cache/src/types/lite.js.map +1 -1
  159. package/out/zero-cache/src/types/pg.d.ts +13 -2
  160. package/out/zero-cache/src/types/pg.d.ts.map +1 -1
  161. package/out/zero-cache/src/types/pg.js +2 -2
  162. package/out/zero-cache/src/types/pg.js.map +1 -1
  163. package/out/zero-cache/src/workers/connection.d.ts +1 -0
  164. package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
  165. package/out/zero-cache/src/workers/connection.js +5 -1
  166. package/out/zero-cache/src/workers/connection.js.map +1 -1
  167. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
  168. package/out/zero-cache/src/workers/syncer-ws-message-handler.js +12 -3
  169. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  170. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  171. package/out/zero-cache/src/workers/syncer.js +15 -14
  172. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  173. package/out/zero-client/src/client/inspector/inspector.d.ts +4 -0
  174. package/out/zero-client/src/client/inspector/inspector.d.ts.map +1 -1
  175. package/out/zero-client/src/client/inspector/inspector.js +4 -0
  176. package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
  177. package/out/zero-client/src/client/inspector/lazy-inspector.d.ts +10 -2
  178. package/out/zero-client/src/client/inspector/lazy-inspector.d.ts.map +1 -1
  179. package/out/zero-client/src/client/inspector/lazy-inspector.js +38 -31
  180. package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
  181. package/out/zero-client/src/client/inspector/query.d.ts +2 -2
  182. package/out/zero-client/src/client/inspector/query.d.ts.map +1 -1
  183. package/out/zero-client/src/client/inspector/query.js +4 -4
  184. package/out/zero-client/src/client/inspector/query.js.map +1 -1
  185. package/out/zero-client/src/client/version.js +1 -1
  186. package/out/zero-client/src/util/nanoid.js +2 -2
  187. package/out/zero-client/src/util/nanoid.js.map +1 -1
  188. package/out/zero-server/src/process-mutations.d.ts +10 -0
  189. package/out/zero-server/src/process-mutations.d.ts.map +1 -1
  190. package/out/zero-server/src/process-mutations.js +38 -5
  191. package/out/zero-server/src/process-mutations.js.map +1 -1
  192. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { InvalidConnectionRequest, Unauthorized } from "../../../../zero-protocol/src/error-kind-enum.js";
1
+ import { InvalidConnectionRequest, Rehome, Unauthorized } from "../../../../zero-protocol/src/error-kind-enum.js";
2
2
  import { ZeroCache } from "../../../../zero-protocol/src/error-origin-enum.js";
3
3
  import { ProtocolErrorWithLevel } from "../../types/error-with-level.js";
4
4
  import { compileUrlPattern } from "../../custom/fetch.js";
@@ -27,9 +27,11 @@ var ConnectionContextManagerImpl = class {
27
27
  #retransformIntervalMs;
28
28
  #queryConfig;
29
29
  #pushConfig;
30
+ #requireServerUserID;
31
+ #warnedUnverifiedUser = false;
30
32
  #sharedRetransformReady = false;
31
33
  #nextInsertionOrder = 0;
32
- constructor(lc, revalidateIntervalSeconds, retransformIntervalSeconds, queryConfig, pushConfig, validateLegacyJWT, now) {
34
+ constructor(lc, revalidateIntervalSeconds, retransformIntervalSeconds, queryConfig, pushConfig, validateLegacyJWT, now, requireServerUserID = false) {
33
35
  this.#lc = lc;
34
36
  this.#now = now ?? Date.now;
35
37
  this.#revalidateIntervalMs = revalidateIntervalSeconds === void 0 ? void 0 : revalidateIntervalSeconds * 1e3;
@@ -37,6 +39,7 @@ var ConnectionContextManagerImpl = class {
37
39
  this.#queryConfig = queryConfig;
38
40
  this.#pushConfig = pushConfig;
39
41
  this.#validateLegacyJWT = validateLegacyJWT;
42
+ this.#requireServerUserID = requireServerUserID;
40
43
  }
41
44
  /**
42
45
  * Creates or replaces the live record for a websocket connection.
@@ -165,6 +168,16 @@ var ConnectionContextManagerImpl = class {
165
168
  message: "Connection userID does not match validated server userID.",
166
169
  origin: ZeroCache
167
170
  }, "warn");
171
+ } else if (validation.kind === "server-unverified" && connection.user.id !== null && connection.auth?.type !== "jwt") {
172
+ if (this.#requireServerUserID) throw new ProtocolErrorWithLevel({
173
+ kind: Unauthorized,
174
+ message: "Connection userID was not validated by the API server. The API server must return a userID for authenticated connections.",
175
+ origin: ZeroCache
176
+ }, "warn");
177
+ if (!this.#warnedUnverifiedUser) {
178
+ this.#warnedUnverifiedUser = true;
179
+ this.#lc.warn?.("The API server answered without a userID, so the userID claimed by the client was not verified. Return the userID from your query and mutate endpoints (zero-server does this when you pass userID to handleQueryRequest and handleMutateRequest), then set ZERO_AUTH_REQUIRE_SERVER_USER_ID=true to refuse unverified userIDs.");
180
+ }
168
181
  }
169
182
  const incomingUserState = validatedUserState ?? connection.user;
170
183
  if (this.#group.pinnedUser !== void 0 && this.#group.pinnedUser.id !== incomingUserState.id) throw new ProtocolErrorWithLevel({
@@ -188,6 +201,31 @@ var ConnectionContextManagerImpl = class {
188
201
  group: this.getGroupState()
189
202
  };
190
203
  }
204
+ /**
205
+ * Pins the group to the user persisted with its CVR, as bound by a validated
206
+ * connection to an earlier instance of the group's view-syncer. Later
207
+ * validations by this instance must match it, as they would had the group
208
+ * been pinned here.
209
+ *
210
+ * Throws a `Rehome` error if the group is already pinned to another user:
211
+ * the stored pin wins, so the connections validated here can no longer be
212
+ * trusted and the view-syncer should stop, leaving its clients to reconnect
213
+ * to a fresh instance that validates them against the stored pin.
214
+ */
215
+ restorePinnedUser(pinnedUser) {
216
+ if (this.#group.pinnedUser === void 0) {
217
+ this.#setGroup({
218
+ ...this.#group,
219
+ pinnedUser: { id: pinnedUser.id }
220
+ });
221
+ return;
222
+ }
223
+ if (this.#group.pinnedUser.id !== pinnedUser.id) throw new ProtocolErrorWithLevel({
224
+ kind: Rehome,
225
+ message: "Client group is pinned to a different userID than the one persisted with its CVR. Reconnect required.",
226
+ origin: ZeroCache
227
+ }, "warn");
228
+ }
191
229
  /** Removes one connection due to failed auth and updates all derived background/deadline state. */
192
230
  failConnection(selector, revision) {
193
231
  return this.#removeConnection(selector, revision);
@@ -1 +1 @@
1
- {"version":3,"file":"connection-context-manager.js","names":["ErrorKind.Unauthorized","ErrorOrigin.ZeroCache","ErrorKind.InvalidConnectionRequest"],"sources":["../../../../../../zero-cache/src/services/view-syncer/connection-context-manager.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {InitConnectionBody} from '../../../../zero-protocol/src/connect.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport type {UpdateAuthBody} from '../../../../zero-protocol/src/update-auth.ts';\nimport {\n authEquals,\n resolveAuth,\n type Auth,\n type ValidateLegacyJWT,\n} from '../../auth/auth.ts';\nimport type {ZeroConfig} from '../../config/zero-config.ts';\nimport {compileUrlPattern} from '../../custom/fetch.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {ConnectParams} from '../../workers/connect-params.ts';\n\nexport type ConnectionState = 'provisional' | 'validated';\n\n/**\n * Normalized user identity shared by live connection state and group auth state.\n * `id: null` means logged out.\n */\nexport type UserState = {readonly id: string | null};\n\n/**\n * Delineates the two paths for validating a connection: either server can validate\n * the user's identity and return a definitive userID to trust, or we fall back to\n * trusting the one provided by the client in the incoming query params.\n */\nexport type ConnectionValidation =\n | {kind: 'client-fallback'}\n | {kind: 'server-validated'; validatedUserID: string | null};\n\n/**\n * Identifies one live websocket for a client slot.\n */\nexport type ConnectionSelector = {\n readonly clientID: string;\n readonly wsID: string;\n};\n\ntype FetchConfig = ZeroConfig['query'];\n\nexport type HeaderOptions = {\n readonly apiKey?: string | undefined;\n /** Allowlisted headers provided in the client options. */\n readonly customHeaders?: Readonly<Record<string, string>> | undefined;\n /** Allowlisted headers from the incoming HTTP request. */\n readonly requestHeaders?: Readonly<Record<string, string>> | undefined;\n readonly cookie?: string | undefined;\n readonly origin?: string | undefined;\n};\n\nexport type ConnectionFetchContext = {\n readonly url: string | undefined;\n readonly allowedUrlPatterns: readonly URLPattern[] | undefined;\n readonly headerOptions: HeaderOptions;\n};\n\n/**\n * A snapshot of one live connection tracked by the manager.\n *\n * `revalidateAt` is only populated while the connection is `validated`.\n */\nexport type ConnectionContext = {\n readonly state: ConnectionState;\n\n readonly clientID: string;\n readonly wsID: string;\n readonly user: UserState;\n\n readonly auth: Auth | undefined;\n\n readonly profileID: string | null;\n readonly baseCookie: string | null;\n readonly protocolVersion: number;\n\n readonly revision: number;\n\n readonly revalidateAt: number | undefined;\n\n readonly insertionOrder: number;\n\n readonly queryContext: ConnectionFetchContext;\n readonly mutateContext: ConnectionFetchContext;\n};\n\n/**\n * Group-scoped auth state shared across the live connections.\n *\n * The background connection is the validated connection currently used for\n * shared background work. Retransform happens on a group level, and uses\n * the background connection's credential to refetch the latest queries.\n */\nexport type GroupAuthState = {\n readonly pinnedUser: UserState | undefined;\n\n readonly backgroundConnection: ConnectionSelector | undefined;\n readonly retransformAt: number | undefined;\n // Defer all maintenance in case a transient failure occurs.\n readonly maintenanceNotBeforeAt: number | undefined;\n};\n\nexport type ConnectionContextManager = {\n registerConnection(\n selector: ConnectionSelector,\n connectParams: ConnectParams,\n auth?: Auth,\n ): Readonly<ConnectionContext>;\n\n initConnection(\n selector: ConnectionSelector,\n body: InitConnectionBody,\n ): Readonly<ConnectionContext>;\n\n updateAuth(\n selector: ConnectionSelector,\n body: UpdateAuthBody,\n ): Promise<Readonly<ConnectionContext>>;\n\n validateConnection(\n selector: ConnectionSelector,\n revision: number,\n validation: ConnectionValidation,\n ):\n | Readonly<{\n connection: ConnectionContext;\n group: GroupAuthState;\n }>\n | undefined;\n\n failConnection(\n selector: ConnectionSelector,\n revision: number,\n ): Readonly<ConnectionContext> | undefined;\n closeConnection(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined;\n\n markBackgroundRetransformSuccess(\n selector: ConnectionSelector,\n revision: number,\n ): void;\n\n setSharedRetransformReady(ready: boolean): void;\n\n deferMaintenance(kind: 'revalidate' | 'retransform'): void;\n\n getConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined;\n mustGetConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext>;\n\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined;\n mustGetBackgroundConnectionContext(): Readonly<ConnectionContext>;\n\n getGroupState(): Readonly<GroupAuthState>;\n\n planMaintenance(): {\n dueRevalidations: Readonly<ConnectionContext>[];\n dueRetransform: boolean;\n earliestDeadlineAt: number | undefined;\n };\n};\n\n/**\n * State machine for the auth state of a single `ViewSyncerService`.\n *\n * Connections are registered as `provisional`, optionally backfilled with\n * `initConnection` metadata, and then promoted to `validated` once their\n * effective `userID` is confirmed as valid. The manager also tracks which\n * validated connection currently serves as the group's background connection.\n */\nexport class ConnectionContextManagerImpl implements ConnectionContextManager {\n readonly #lc: LogContext;\n\n // The live connection records, keyed by clientID\n readonly #connections = new Map<string, ConnectionContext>();\n #group: GroupAuthState = {\n pinnedUser: undefined,\n backgroundConnection: undefined,\n retransformAt: undefined,\n maintenanceNotBeforeAt: undefined,\n };\n\n readonly #validateLegacyJWT: ValidateLegacyJWT | undefined;\n\n readonly #now: () => number;\n readonly #revalidateIntervalMs: number | undefined;\n readonly #retransformIntervalMs: number | undefined;\n readonly #queryConfig: FetchConfig | undefined;\n readonly #pushConfig: FetchConfig | undefined;\n #sharedRetransformReady = false;\n #nextInsertionOrder = 0;\n\n constructor(\n lc: LogContext,\n revalidateIntervalSeconds?: number,\n retransformIntervalSeconds?: number,\n queryConfig?: FetchConfig,\n pushConfig?: FetchConfig,\n validateLegacyJWT?: ValidateLegacyJWT,\n now?: () => number,\n ) {\n this.#lc = lc;\n this.#now = now ?? Date.now;\n this.#revalidateIntervalMs =\n revalidateIntervalSeconds === undefined\n ? undefined\n : revalidateIntervalSeconds * 1000;\n this.#retransformIntervalMs =\n retransformIntervalSeconds === undefined\n ? undefined\n : retransformIntervalSeconds * 1000;\n this.#queryConfig = queryConfig;\n this.#pushConfig = pushConfig;\n this.#validateLegacyJWT = validateLegacyJWT;\n }\n\n /**\n * Creates or replaces the live record for a websocket connection.\n *\n * Re-registering the same `clientID` drops the old socket record and starts\n * the replacement back in `provisional` state.\n */\n registerConnection(\n selector: ConnectionSelector,\n connectParams: ConnectParams,\n auth?: Auth,\n ): Readonly<ConnectionContext> {\n this.#removeConnection(selector);\n\n const getContext = (type: 'query' | 'mutate'): ConnectionFetchContext => {\n const config = type === 'query' ? this.#queryConfig : this.#pushConfig;\n\n return {\n url: config?.url?.[0],\n allowedUrlPatterns: config?.url?.map(compileUrlPattern),\n headerOptions: {\n customHeaders: undefined,\n requestHeaders: filterHeaders(\n connectParams.requestHeaders,\n config?.allowedRequestHeaders,\n ),\n origin: connectParams.origin,\n apiKey: config?.apiKey,\n cookie: config?.forwardCookies ? connectParams.httpCookie : undefined,\n },\n };\n };\n\n const connection: ConnectionContext = {\n state: 'provisional',\n\n clientID: connectParams.clientID,\n wsID: connectParams.wsID,\n revision: 0,\n user: {id: connectParams.userID ?? null},\n auth,\n\n profileID: connectParams.profileID,\n baseCookie: connectParams.baseCookie,\n protocolVersion: connectParams.protocolVersion,\n\n revalidateAt: undefined,\n\n queryContext: getContext('query'),\n mutateContext: getContext('mutate'),\n\n insertionOrder: ++this.#nextInsertionOrder,\n };\n this.#storeConnection(connection);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return connection;\n }\n\n /**\n * Backfills `initConnection` data for sockets that were registered before the\n * client could send its full init payload.\n *\n * This updates metadata only; it does not validate the connection.\n */\n initConnection(\n selector: ConnectionSelector,\n body: InitConnectionBody,\n ): Readonly<ConnectionContext> {\n const connection = this.#mustGetConnectionContext(selector);\n\n let queryContext = connection.queryContext;\n let mutateContext = connection.mutateContext;\n\n if (body.userQueryURL) {\n queryContext = {\n ...queryContext,\n url: body.userQueryURL,\n };\n }\n if (body.userQueryHeaders) {\n queryContext = {\n ...queryContext,\n headerOptions: {\n ...queryContext.headerOptions,\n customHeaders: filterHeaders(\n body.userQueryHeaders,\n this.#queryConfig?.allowedClientHeaders,\n ),\n },\n };\n }\n if (body.userPushURL) {\n mutateContext = {\n ...mutateContext,\n url: body.userPushURL,\n };\n }\n if (body.userPushHeaders) {\n mutateContext = {\n ...mutateContext,\n headerOptions: {\n ...mutateContext.headerOptions,\n customHeaders: filterHeaders(\n body.userPushHeaders,\n this.#pushConfig?.allowedClientHeaders,\n ),\n },\n };\n }\n\n return this.#demoteConnection({\n ...connection,\n revision: connection.revision + 1,\n queryContext,\n mutateContext,\n });\n }\n\n /**\n * A material auth change demotes the connection back to provisional until it\n * is validated again.\n */\n async updateAuth(\n selector: ConnectionSelector,\n body: UpdateAuthBody,\n ): Promise<Readonly<ConnectionContext>> {\n const connection = this.#mustGetConnectionContext(selector);\n\n const nextAuth = await resolveAuth(\n this.#lc,\n connection.auth,\n connection.user.id,\n body.auth,\n this.#validateLegacyJWT,\n );\n\n const authChanged = !authEquals(connection.auth, nextAuth);\n if (authChanged) {\n return this.#demoteConnection({\n ...connection,\n auth: nextAuth,\n revision: connection.revision + 1,\n });\n }\n\n if (nextAuth === connection.auth) {\n return connection;\n }\n\n return this.#storeConnection({\n ...connection,\n auth: nextAuth,\n });\n }\n\n /**\n * Validates one connection against the group's pinned `userID`.\n *\n * The first successful validation binds the group `userID`. Later\n * validations must match it. Validation also refreshes the connection's\n * revalidation deadline and may pick the connection as the group\n * background connection if none is currently available. If the websocket is\n * gone by the time async validation finishes, this becomes a no-op.\n */\n validateConnection(\n selector: ConnectionSelector,\n revision: number,\n validation: ConnectionValidation,\n ):\n | Readonly<{\n connection: ConnectionContext;\n group: GroupAuthState;\n }>\n | undefined {\n const connection = this.#getConnectionContext(selector);\n if (!connection) {\n return undefined;\n }\n\n if (connection.revision !== revision) {\n this.#lc.debug?.('Skipping validateConnection for stale revision', {\n clientID: selector.clientID,\n attemptedRevision: revision,\n currentRevision: connection.revision,\n });\n return undefined;\n }\n\n let validatedUserState: UserState | undefined;\n\n // If the API server has validated the user's identity, we ensure that\n // the connection's claimed userID matches it.\n if (validation.kind === 'server-validated') {\n validatedUserState = {id: validation.validatedUserID};\n\n // Check that the ws connection userID provided by the client\n // matches the validated userID from the API server.\n if (connection.user.id !== validatedUserState.id) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Connection userID does not match validated server userID.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n }\n\n // The incoming user state is either the validated user state from the server\n // or the WS client's claimed user state if no server validation occurred.\n const incomingUserState = validatedUserState ?? connection.user;\n\n // Once a client group is validated, every later validated connection must\n // agree with that pinned identity.\n if (\n this.#group.pinnedUser !== undefined &&\n this.#group.pinnedUser.id !== incomingUserState.id\n ) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Client groups are pinned to a single userID. Connection userID does not match existing client group userID.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n\n if (this.#group.pinnedUser === undefined) {\n this.#setGroup({\n ...this.#group,\n pinnedUser: incomingUserState,\n });\n }\n\n const validatedConnection = this.#storeConnection({\n ...connection,\n state: 'validated',\n revalidateAt: this.#nextRevalidateAt(),\n });\n this.#refreshBackgroundConnectionContext(validatedConnection);\n this.#updateBackgroundRetransformDeadline(false);\n\n return {\n connection: validatedConnection,\n group: this.getGroupState(),\n };\n }\n\n /** Removes one connection due to failed auth and updates all derived background/deadline state. */\n failConnection(\n selector: ConnectionSelector,\n revision: number,\n ): ConnectionContext | undefined {\n return this.#removeConnection(selector, revision);\n }\n\n /** Removes one disconnected connection and updates all derived background/deadline state. */\n closeConnection(selector: ConnectionSelector): ConnectionContext | undefined {\n return this.#removeConnection(selector);\n }\n\n /**\n * Records a successful background retransform. This starts a fresh interval\n * from the manager clock when shared retransform is schedulable, or\n * clears the deadline if it is not.\n */\n markBackgroundRetransformSuccess(\n selector: ConnectionSelector,\n revision: number,\n ): void {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection) {\n return;\n }\n if (\n backgroundConnection.clientID !== selector.clientID ||\n backgroundConnection.wsID !== selector.wsID ||\n backgroundConnection.revision !== revision\n ) {\n return;\n }\n this.#updateBackgroundRetransformDeadline(true);\n }\n\n setSharedRetransformReady(ready: boolean): void {\n if (this.#sharedRetransformReady === ready) {\n return;\n }\n this.#sharedRetransformReady = ready;\n this.#updateBackgroundRetransformDeadline(true);\n }\n\n deferMaintenance(kind: 'revalidate' | 'retransform'): void {\n const intervalMs =\n kind === 'revalidate'\n ? this.#revalidateIntervalMs\n : this.#retransformIntervalMs;\n if (intervalMs === undefined) {\n return;\n }\n this.#setGroup({\n ...this.#group,\n maintenanceNotBeforeAt: Math.max(\n this.#group.maintenanceNotBeforeAt ?? 0,\n this.#now() + intervalMs,\n ),\n });\n }\n\n /** Returns the current live record for a client slot, if any. */\n getConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined {\n return this.#getConnectionContext(selector);\n }\n\n /** Returns the live record for one websocket or throws if it is unavailable. */\n mustGetConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> {\n return this.#mustGetConnectionContext(selector);\n }\n\n /** Returns the current background connection, if one exists. */\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined {\n return this.#getBackgroundConnectionContext();\n }\n\n mustGetBackgroundConnectionContext(): Readonly<ConnectionContext> {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.InvalidConnectionRequest,\n message:\n 'No validated connection is available for shared query work.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n return backgroundConnection;\n }\n\n /** Returns the shared group auth state. */\n getGroupState(): Readonly<GroupAuthState> {\n return this.#group;\n }\n\n /**\n * Reports which maintenance work is currently due.\n *\n * The result is a pure snapshot: callers decide which actions to run and\n * when to wake up next. `earliestDeadlineAt` is the earliest outstanding\n * maintenance deadline, including overdue work, unless a transient failure\n * has deferred all scheduled maintenance until `maintenanceNotBeforeAt`.\n */\n planMaintenance(): {\n dueRevalidations: Readonly<ConnectionContext>[];\n dueRetransform: boolean;\n earliestDeadlineAt: number | undefined;\n } {\n const dueRevalidations: Readonly<ConnectionContext>[] = [];\n const now = this.#now();\n let earliestDeadlineAt = this.#group.retransformAt;\n\n for (const connection of this.#connections.values()) {\n if (\n connection.state !== 'validated' ||\n connection.revalidateAt === undefined\n ) {\n continue;\n }\n if (connection.revalidateAt <= now) {\n dueRevalidations.push(connection);\n }\n earliestDeadlineAt = minDefined(\n earliestDeadlineAt,\n connection.revalidateAt,\n );\n }\n\n const dueRetransform =\n this.#group.retransformAt !== undefined &&\n this.#group.retransformAt <= now;\n const maintenanceNotBeforeAt = this.#group.maintenanceNotBeforeAt;\n\n if (\n maintenanceNotBeforeAt !== undefined &&\n maintenanceNotBeforeAt > now &&\n earliestDeadlineAt !== undefined\n ) {\n return {\n dueRevalidations: [],\n dueRetransform: false,\n earliestDeadlineAt: Math.max(\n earliestDeadlineAt,\n maintenanceNotBeforeAt,\n ),\n };\n }\n\n return {\n dueRevalidations: dueRevalidations.sort(compareByInsertionOrder),\n dueRetransform,\n earliestDeadlineAt,\n };\n }\n\n #removeConnection(\n selector: ConnectionSelector,\n revision?: number,\n ): Readonly<ConnectionContext> | undefined {\n const connection = this.#getConnectionContext(selector);\n\n if (!connection) {\n return undefined;\n }\n\n // If the revision has changed, we should not remove the connection\n if (revision !== undefined && connection.revision !== revision) {\n this.#lc.debug?.('Ignoring failConnection for stale revision', {\n clientID: selector.clientID,\n wsID: selector.wsID,\n attemptedRevision: revision,\n currentRevision: connection.revision,\n });\n return undefined;\n }\n\n this.#connections.delete(connection.clientID);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n\n return connection;\n }\n\n #demoteConnection(connection: ConnectionContext): ConnectionContext {\n const demotedConnection = this.#storeConnection({\n ...connection,\n state: 'provisional',\n revalidateAt: undefined,\n });\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return demotedConnection;\n }\n\n /**\n * Keeps the background connection sticky while it remains validated.\n *\n * When a newly validated `preferred` connection is provided, it is promoted\n * only if there is no current validated background connection. Otherwise the\n * existing background connection stays in place until it disappears or is\n * demoted, at which point the newest validated connection is selected.\n */\n #refreshBackgroundConnectionContext(preferred?: ConnectionContext): void {\n if (preferred?.state === 'validated') {\n const currentBackgroundConnection =\n this.#getBackgroundConnectionContext();\n if (\n currentBackgroundConnection?.clientID === preferred.clientID &&\n currentBackgroundConnection.wsID === preferred.wsID\n ) {\n return;\n }\n if (currentBackgroundConnection !== undefined) {\n return;\n }\n this.#setBackgroundConnection({\n clientID: preferred.clientID,\n wsID: preferred.wsID,\n });\n this.#lc.debug?.('Selected background connection for shared auth work', {\n clientID: preferred.clientID,\n wsID: preferred.wsID,\n revision: preferred.revision,\n reason: 'preferred-validated',\n });\n return;\n }\n\n const currentBackgroundConnection = this.#getBackgroundConnectionContext();\n if (currentBackgroundConnection?.state === 'validated') {\n return;\n }\n\n const nextBackgroundConnection = [...this.#connections.values()]\n .filter(connection => connection.state === 'validated')\n .sort(comparePreferredValidatedConnection)\n .at(0);\n this.#setBackgroundConnection(\n nextBackgroundConnection\n ? {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n }\n : undefined,\n );\n if (nextBackgroundConnection) {\n this.#lc.debug?.('Selected background connection for shared auth work', {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n revision: nextBackgroundConnection.revision,\n reason: 'fallback-validated',\n });\n }\n }\n\n #getBackgroundConnectionContext(): ConnectionContext | undefined {\n const backgroundConnection = this.#group.backgroundConnection;\n if (!backgroundConnection) {\n return undefined;\n }\n return this.#getConnectionContext(backgroundConnection);\n }\n\n #getConnectionContext(\n selector: ConnectionSelector,\n ): ConnectionContext | undefined {\n const connection = this.#connections.get(selector.clientID);\n if (!connection) {\n return undefined;\n }\n if (connection.wsID !== selector.wsID) {\n return undefined;\n }\n return connection;\n }\n\n #mustGetConnectionContext(selector: ConnectionSelector): ConnectionContext {\n const connection = this.#getConnectionContext(selector);\n\n if (!connection) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.InvalidConnectionRequest,\n message:\n 'Connection auth state was not available for this websocket.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n\n return connection;\n }\n\n #storeConnection(connection: ConnectionContext): ConnectionContext {\n this.#connections.set(connection.clientID, connection);\n return connection;\n }\n\n #setGroup(group: GroupAuthState): GroupAuthState {\n this.#group = group;\n return group;\n }\n\n #setBackgroundConnection(\n backgroundConnection: ConnectionSelector | undefined,\n ) {\n if (\n sameConnectionSelector(\n this.#group.backgroundConnection,\n backgroundConnection,\n )\n ) {\n return;\n }\n this.#setGroup({\n ...this.#group,\n backgroundConnection: backgroundConnection\n ? {...backgroundConnection}\n : undefined,\n });\n }\n\n /**\n * Keeps the group background retransform deadline coherent with current\n * schedulability.\n *\n * When `reset` is false, this seeds a deadline only when shared retransform\n * is now possible and no deadline exists yet, preserving any existing\n * cadence. When `reset` is true, it starts a fresh interval from `#now()` if\n * retransform is schedulable for the current ready ViewSyncer instance, or\n * clears the deadline if it is not.\n */\n #updateBackgroundRetransformDeadline(reset: boolean) {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (\n !backgroundConnection ||\n this.#retransformIntervalMs === undefined ||\n !this.#sharedRetransformReady\n ) {\n if (this.#group.retransformAt !== undefined) {\n this.#setGroup({\n ...this.#group,\n retransformAt: undefined,\n });\n }\n return;\n }\n\n if (reset || this.#group.retransformAt === undefined) {\n this.#setGroup({\n ...this.#group,\n retransformAt: this.#now() + this.#retransformIntervalMs,\n });\n }\n }\n\n #nextRevalidateAt() {\n return this.#revalidateIntervalMs === undefined\n ? undefined\n : this.#now() + this.#revalidateIntervalMs;\n }\n}\n\nfunction compareByInsertionOrder(\n a: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n b: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n) {\n return a.insertionOrder - b.insertionOrder || a.wsID.localeCompare(b.wsID);\n}\n\nfunction comparePreferredValidatedConnection(\n a: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n b: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n) {\n return b.insertionOrder - a.insertionOrder || b.wsID.localeCompare(a.wsID);\n}\n\nfunction minDefined(a: number | undefined, b: number | undefined) {\n if (a === undefined) {\n return b;\n }\n if (b === undefined) {\n return a;\n }\n return Math.min(a, b);\n}\n\nfunction sameConnectionSelector(\n a: ConnectionSelector | undefined,\n b: ConnectionSelector | undefined,\n) {\n return a?.clientID === b?.clientID && a?.wsID === b?.wsID;\n}\n\nfunction filterHeaders(\n headers: Readonly<Record<string, string>> | undefined,\n allowedHeaders: readonly string[] | undefined,\n) {\n if (!headers || !allowedHeaders || allowedHeaders.length === 0) {\n return undefined;\n }\n\n const allowed = new Set(allowedHeaders.map(header => header.toLowerCase()));\n let filtered: Record<string, string> | undefined;\n for (const [key, value] of Object.entries(headers)) {\n if (allowed.has(key.toLowerCase())) {\n filtered ??= {};\n filtered[key] = value;\n }\n }\n return filtered;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"connection-context-manager.js","names":["ErrorKind.Unauthorized","ErrorOrigin.ZeroCache","ErrorKind.Rehome","ErrorKind.InvalidConnectionRequest"],"sources":["../../../../../../zero-cache/src/services/view-syncer/connection-context-manager.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {InitConnectionBody} from '../../../../zero-protocol/src/connect.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport type {UpdateAuthBody} from '../../../../zero-protocol/src/update-auth.ts';\nimport {\n authEquals,\n resolveAuth,\n type Auth,\n type ValidateLegacyJWT,\n} from '../../auth/auth.ts';\nimport type {ZeroConfig} from '../../config/zero-config.ts';\nimport {compileUrlPattern} from '../../custom/fetch.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {ConnectParams} from '../../workers/connect-params.ts';\n\nexport type ConnectionState = 'provisional' | 'validated';\n\n/**\n * Normalized user identity shared by live connection state and group auth state.\n * `id: null` means logged out.\n */\nexport type UserState = {readonly id: string | null};\n\n/**\n * How a connection's `userID` was established:\n *\n * - `server-validated`: the API server returned the authoritative userID\n * (`null` for a logged-out caller). The connection's claimed userID must\n * match it.\n * - `server-unverified`: the API server answered without a userID, so it did\n * not vouch for the caller. With `requireServerUserID`, only a logged-out\n * connection, or a userID that a legacy JWT has already verified, is\n * accepted. Without it, the claimed userID is trusted and a warning is\n * logged.\n * - `client-fallback`: no API server is involved, so the userID provided by\n * the client in the connection params is trusted as-is (a legacy JWT, when\n * configured, has verified it).\n */\nexport type ConnectionValidation =\n | {kind: 'client-fallback'}\n | {kind: 'server-unverified'}\n | {kind: 'server-validated'; validatedUserID: string | null};\n\n/**\n * Identifies one live websocket for a client slot.\n */\nexport type ConnectionSelector = {\n readonly clientID: string;\n readonly wsID: string;\n};\n\ntype FetchConfig = ZeroConfig['query'];\n\nexport type HeaderOptions = {\n readonly apiKey?: string | undefined;\n /** Allowlisted headers provided in the client options. */\n readonly customHeaders?: Readonly<Record<string, string>> | undefined;\n /** Allowlisted headers from the incoming HTTP request. */\n readonly requestHeaders?: Readonly<Record<string, string>> | undefined;\n readonly cookie?: string | undefined;\n readonly origin?: string | undefined;\n};\n\nexport type ConnectionFetchContext = {\n readonly url: string | undefined;\n readonly allowedUrlPatterns: readonly URLPattern[] | undefined;\n readonly headerOptions: HeaderOptions;\n};\n\n/**\n * A snapshot of one live connection tracked by the manager.\n *\n * `revalidateAt` is only populated while the connection is `validated`.\n */\nexport type ConnectionContext = {\n readonly state: ConnectionState;\n\n readonly clientID: string;\n readonly wsID: string;\n readonly user: UserState;\n\n readonly auth: Auth | undefined;\n\n readonly profileID: string | null;\n readonly baseCookie: string | null;\n readonly protocolVersion: number;\n\n readonly revision: number;\n\n readonly revalidateAt: number | undefined;\n\n readonly insertionOrder: number;\n\n readonly queryContext: ConnectionFetchContext;\n readonly mutateContext: ConnectionFetchContext;\n};\n\n/**\n * Group-scoped auth state shared across the live connections.\n *\n * The background connection is the validated connection currently used for\n * shared background work. Retransform happens on a group level, and uses\n * the background connection's credential to refetch the latest queries.\n */\nexport type GroupAuthState = {\n readonly pinnedUser: UserState | undefined;\n\n readonly backgroundConnection: ConnectionSelector | undefined;\n readonly retransformAt: number | undefined;\n // Defer all maintenance in case a transient failure occurs.\n readonly maintenanceNotBeforeAt: number | undefined;\n};\n\nexport type ConnectionContextManager = {\n registerConnection(\n selector: ConnectionSelector,\n connectParams: ConnectParams,\n auth?: Auth,\n ): Readonly<ConnectionContext>;\n\n initConnection(\n selector: ConnectionSelector,\n body: InitConnectionBody,\n ): Readonly<ConnectionContext>;\n\n updateAuth(\n selector: ConnectionSelector,\n body: UpdateAuthBody,\n ): Promise<Readonly<ConnectionContext>>;\n\n validateConnection(\n selector: ConnectionSelector,\n revision: number,\n validation: ConnectionValidation,\n ):\n | Readonly<{\n connection: ConnectionContext;\n group: GroupAuthState;\n }>\n | undefined;\n\n restorePinnedUser(pinnedUser: UserState): void;\n\n failConnection(\n selector: ConnectionSelector,\n revision: number,\n ): Readonly<ConnectionContext> | undefined;\n closeConnection(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined;\n\n markBackgroundRetransformSuccess(\n selector: ConnectionSelector,\n revision: number,\n ): void;\n\n setSharedRetransformReady(ready: boolean): void;\n\n deferMaintenance(kind: 'revalidate' | 'retransform'): void;\n\n getConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined;\n mustGetConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext>;\n\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined;\n mustGetBackgroundConnectionContext(): Readonly<ConnectionContext>;\n\n getGroupState(): Readonly<GroupAuthState>;\n\n planMaintenance(): {\n dueRevalidations: Readonly<ConnectionContext>[];\n dueRetransform: boolean;\n earliestDeadlineAt: number | undefined;\n };\n};\n\n/**\n * State machine for the auth state of a single `ViewSyncerService`.\n *\n * Connections are registered as `provisional`, optionally backfilled with\n * `initConnection` metadata, and then promoted to `validated` once their\n * effective `userID` is confirmed as valid. The manager also tracks which\n * validated connection currently serves as the group's background connection.\n */\nexport class ConnectionContextManagerImpl implements ConnectionContextManager {\n readonly #lc: LogContext;\n\n // The live connection records, keyed by clientID\n readonly #connections = new Map<string, ConnectionContext>();\n #group: GroupAuthState = {\n pinnedUser: undefined,\n backgroundConnection: undefined,\n retransformAt: undefined,\n maintenanceNotBeforeAt: undefined,\n };\n\n readonly #validateLegacyJWT: ValidateLegacyJWT | undefined;\n\n readonly #now: () => number;\n readonly #revalidateIntervalMs: number | undefined;\n readonly #retransformIntervalMs: number | undefined;\n readonly #queryConfig: FetchConfig | undefined;\n readonly #pushConfig: FetchConfig | undefined;\n readonly #requireServerUserID: boolean;\n #warnedUnverifiedUser = false;\n #sharedRetransformReady = false;\n #nextInsertionOrder = 0;\n\n constructor(\n lc: LogContext,\n revalidateIntervalSeconds?: number,\n retransformIntervalSeconds?: number,\n queryConfig?: FetchConfig,\n pushConfig?: FetchConfig,\n validateLegacyJWT?: ValidateLegacyJWT,\n now?: () => number,\n requireServerUserID = false,\n ) {\n this.#lc = lc;\n this.#now = now ?? Date.now;\n this.#revalidateIntervalMs =\n revalidateIntervalSeconds === undefined\n ? undefined\n : revalidateIntervalSeconds * 1000;\n this.#retransformIntervalMs =\n retransformIntervalSeconds === undefined\n ? undefined\n : retransformIntervalSeconds * 1000;\n this.#queryConfig = queryConfig;\n this.#pushConfig = pushConfig;\n this.#validateLegacyJWT = validateLegacyJWT;\n this.#requireServerUserID = requireServerUserID;\n }\n\n /**\n * Creates or replaces the live record for a websocket connection.\n *\n * Re-registering the same `clientID` drops the old socket record and starts\n * the replacement back in `provisional` state.\n */\n registerConnection(\n selector: ConnectionSelector,\n connectParams: ConnectParams,\n auth?: Auth,\n ): Readonly<ConnectionContext> {\n this.#removeConnection(selector);\n\n const getContext = (type: 'query' | 'mutate'): ConnectionFetchContext => {\n const config = type === 'query' ? this.#queryConfig : this.#pushConfig;\n\n return {\n url: config?.url?.[0],\n allowedUrlPatterns: config?.url?.map(compileUrlPattern),\n headerOptions: {\n customHeaders: undefined,\n requestHeaders: filterHeaders(\n connectParams.requestHeaders,\n config?.allowedRequestHeaders,\n ),\n origin: connectParams.origin,\n apiKey: config?.apiKey,\n cookie: config?.forwardCookies ? connectParams.httpCookie : undefined,\n },\n };\n };\n\n const connection: ConnectionContext = {\n state: 'provisional',\n\n clientID: connectParams.clientID,\n wsID: connectParams.wsID,\n revision: 0,\n user: {id: connectParams.userID ?? null},\n auth,\n\n profileID: connectParams.profileID,\n baseCookie: connectParams.baseCookie,\n protocolVersion: connectParams.protocolVersion,\n\n revalidateAt: undefined,\n\n queryContext: getContext('query'),\n mutateContext: getContext('mutate'),\n\n insertionOrder: ++this.#nextInsertionOrder,\n };\n this.#storeConnection(connection);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return connection;\n }\n\n /**\n * Backfills `initConnection` data for sockets that were registered before the\n * client could send its full init payload.\n *\n * This updates metadata only; it does not validate the connection.\n */\n initConnection(\n selector: ConnectionSelector,\n body: InitConnectionBody,\n ): Readonly<ConnectionContext> {\n const connection = this.#mustGetConnectionContext(selector);\n\n let queryContext = connection.queryContext;\n let mutateContext = connection.mutateContext;\n\n if (body.userQueryURL) {\n queryContext = {\n ...queryContext,\n url: body.userQueryURL,\n };\n }\n if (body.userQueryHeaders) {\n queryContext = {\n ...queryContext,\n headerOptions: {\n ...queryContext.headerOptions,\n customHeaders: filterHeaders(\n body.userQueryHeaders,\n this.#queryConfig?.allowedClientHeaders,\n ),\n },\n };\n }\n if (body.userPushURL) {\n mutateContext = {\n ...mutateContext,\n url: body.userPushURL,\n };\n }\n if (body.userPushHeaders) {\n mutateContext = {\n ...mutateContext,\n headerOptions: {\n ...mutateContext.headerOptions,\n customHeaders: filterHeaders(\n body.userPushHeaders,\n this.#pushConfig?.allowedClientHeaders,\n ),\n },\n };\n }\n\n return this.#demoteConnection({\n ...connection,\n revision: connection.revision + 1,\n queryContext,\n mutateContext,\n });\n }\n\n /**\n * A material auth change demotes the connection back to provisional until it\n * is validated again.\n */\n async updateAuth(\n selector: ConnectionSelector,\n body: UpdateAuthBody,\n ): Promise<Readonly<ConnectionContext>> {\n const connection = this.#mustGetConnectionContext(selector);\n\n const nextAuth = await resolveAuth(\n this.#lc,\n connection.auth,\n connection.user.id,\n body.auth,\n this.#validateLegacyJWT,\n );\n\n const authChanged = !authEquals(connection.auth, nextAuth);\n if (authChanged) {\n return this.#demoteConnection({\n ...connection,\n auth: nextAuth,\n revision: connection.revision + 1,\n });\n }\n\n if (nextAuth === connection.auth) {\n return connection;\n }\n\n return this.#storeConnection({\n ...connection,\n auth: nextAuth,\n });\n }\n\n /**\n * Validates one connection against the group's pinned `userID`.\n *\n * The first successful validation binds the group `userID`. Later\n * validations must match it. Validation also refreshes the connection's\n * revalidation deadline and may pick the connection as the group\n * background connection if none is currently available. If the websocket is\n * gone by the time async validation finishes, this becomes a no-op.\n */\n validateConnection(\n selector: ConnectionSelector,\n revision: number,\n validation: ConnectionValidation,\n ):\n | Readonly<{\n connection: ConnectionContext;\n group: GroupAuthState;\n }>\n | undefined {\n const connection = this.#getConnectionContext(selector);\n if (!connection) {\n return undefined;\n }\n\n if (connection.revision !== revision) {\n this.#lc.debug?.('Skipping validateConnection for stale revision', {\n clientID: selector.clientID,\n attemptedRevision: revision,\n currentRevision: connection.revision,\n });\n return undefined;\n }\n\n let validatedUserState: UserState | undefined;\n\n // If the API server has validated the user's identity, we ensure that\n // the connection's claimed userID matches it.\n if (validation.kind === 'server-validated') {\n validatedUserState = {id: validation.validatedUserID};\n\n // Check that the ws connection userID provided by the client\n // matches the validated userID from the API server.\n if (connection.user.id !== validatedUserState.id) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Connection userID does not match validated server userID.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n } else if (\n validation.kind === 'server-unverified' &&\n connection.user.id !== null &&\n connection.auth?.type !== 'jwt'\n ) {\n // The API server did not name the user, so nothing backs the claimed\n // userID. Trusting it would let a connection pin the client group to a\n // userID it does not own.\n if (this.#requireServerUserID) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Connection userID was not validated by the API server. The API server must return a userID for authenticated connections.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n if (!this.#warnedUnverifiedUser) {\n this.#warnedUnverifiedUser = true;\n this.#lc.warn?.(\n 'The API server answered without a userID, so the userID claimed by the client was not verified. ' +\n 'Return the userID from your query and mutate endpoints (zero-server does this when you pass userID to handleQueryRequest and handleMutateRequest), ' +\n 'then set ZERO_AUTH_REQUIRE_SERVER_USER_ID=true to refuse unverified userIDs.',\n );\n }\n }\n\n // The incoming user state is either the validated user state from the server\n // or the WS client's claimed user state if no server validation occurred.\n const incomingUserState = validatedUserState ?? connection.user;\n\n // Once a client group is validated, every later validated connection must\n // agree with that pinned identity.\n if (\n this.#group.pinnedUser !== undefined &&\n this.#group.pinnedUser.id !== incomingUserState.id\n ) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Unauthorized,\n message:\n 'Client groups are pinned to a single userID. Connection userID does not match existing client group userID.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n\n if (this.#group.pinnedUser === undefined) {\n this.#setGroup({\n ...this.#group,\n pinnedUser: incomingUserState,\n });\n }\n\n const validatedConnection = this.#storeConnection({\n ...connection,\n state: 'validated',\n revalidateAt: this.#nextRevalidateAt(),\n });\n this.#refreshBackgroundConnectionContext(validatedConnection);\n this.#updateBackgroundRetransformDeadline(false);\n\n return {\n connection: validatedConnection,\n group: this.getGroupState(),\n };\n }\n\n /**\n * Pins the group to the user persisted with its CVR, as bound by a validated\n * connection to an earlier instance of the group's view-syncer. Later\n * validations by this instance must match it, as they would had the group\n * been pinned here.\n *\n * Throws a `Rehome` error if the group is already pinned to another user:\n * the stored pin wins, so the connections validated here can no longer be\n * trusted and the view-syncer should stop, leaving its clients to reconnect\n * to a fresh instance that validates them against the stored pin.\n */\n restorePinnedUser(pinnedUser: UserState): void {\n if (this.#group.pinnedUser === undefined) {\n this.#setGroup({...this.#group, pinnedUser: {id: pinnedUser.id}});\n return;\n }\n if (this.#group.pinnedUser.id !== pinnedUser.id) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Rehome,\n message:\n 'Client group is pinned to a different userID than the one persisted with its CVR. Reconnect required.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n }\n\n /** Removes one connection due to failed auth and updates all derived background/deadline state. */\n failConnection(\n selector: ConnectionSelector,\n revision: number,\n ): ConnectionContext | undefined {\n return this.#removeConnection(selector, revision);\n }\n\n /** Removes one disconnected connection and updates all derived background/deadline state. */\n closeConnection(selector: ConnectionSelector): ConnectionContext | undefined {\n return this.#removeConnection(selector);\n }\n\n /**\n * Records a successful background retransform. This starts a fresh interval\n * from the manager clock when shared retransform is schedulable, or\n * clears the deadline if it is not.\n */\n markBackgroundRetransformSuccess(\n selector: ConnectionSelector,\n revision: number,\n ): void {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection) {\n return;\n }\n if (\n backgroundConnection.clientID !== selector.clientID ||\n backgroundConnection.wsID !== selector.wsID ||\n backgroundConnection.revision !== revision\n ) {\n return;\n }\n this.#updateBackgroundRetransformDeadline(true);\n }\n\n setSharedRetransformReady(ready: boolean): void {\n if (this.#sharedRetransformReady === ready) {\n return;\n }\n this.#sharedRetransformReady = ready;\n this.#updateBackgroundRetransformDeadline(true);\n }\n\n deferMaintenance(kind: 'revalidate' | 'retransform'): void {\n const intervalMs =\n kind === 'revalidate'\n ? this.#revalidateIntervalMs\n : this.#retransformIntervalMs;\n if (intervalMs === undefined) {\n return;\n }\n this.#setGroup({\n ...this.#group,\n maintenanceNotBeforeAt: Math.max(\n this.#group.maintenanceNotBeforeAt ?? 0,\n this.#now() + intervalMs,\n ),\n });\n }\n\n /** Returns the current live record for a client slot, if any. */\n getConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> | undefined {\n return this.#getConnectionContext(selector);\n }\n\n /** Returns the live record for one websocket or throws if it is unavailable. */\n mustGetConnectionContext(\n selector: ConnectionSelector,\n ): Readonly<ConnectionContext> {\n return this.#mustGetConnectionContext(selector);\n }\n\n /** Returns the current background connection, if one exists. */\n getBackgroundConnectionContext(): Readonly<ConnectionContext> | undefined {\n return this.#getBackgroundConnectionContext();\n }\n\n mustGetBackgroundConnectionContext(): Readonly<ConnectionContext> {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (!backgroundConnection) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.InvalidConnectionRequest,\n message:\n 'No validated connection is available for shared query work.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n return backgroundConnection;\n }\n\n /** Returns the shared group auth state. */\n getGroupState(): Readonly<GroupAuthState> {\n return this.#group;\n }\n\n /**\n * Reports which maintenance work is currently due.\n *\n * The result is a pure snapshot: callers decide which actions to run and\n * when to wake up next. `earliestDeadlineAt` is the earliest outstanding\n * maintenance deadline, including overdue work, unless a transient failure\n * has deferred all scheduled maintenance until `maintenanceNotBeforeAt`.\n */\n planMaintenance(): {\n dueRevalidations: Readonly<ConnectionContext>[];\n dueRetransform: boolean;\n earliestDeadlineAt: number | undefined;\n } {\n const dueRevalidations: Readonly<ConnectionContext>[] = [];\n const now = this.#now();\n let earliestDeadlineAt = this.#group.retransformAt;\n\n for (const connection of this.#connections.values()) {\n if (\n connection.state !== 'validated' ||\n connection.revalidateAt === undefined\n ) {\n continue;\n }\n if (connection.revalidateAt <= now) {\n dueRevalidations.push(connection);\n }\n earliestDeadlineAt = minDefined(\n earliestDeadlineAt,\n connection.revalidateAt,\n );\n }\n\n const dueRetransform =\n this.#group.retransformAt !== undefined &&\n this.#group.retransformAt <= now;\n const maintenanceNotBeforeAt = this.#group.maintenanceNotBeforeAt;\n\n if (\n maintenanceNotBeforeAt !== undefined &&\n maintenanceNotBeforeAt > now &&\n earliestDeadlineAt !== undefined\n ) {\n return {\n dueRevalidations: [],\n dueRetransform: false,\n earliestDeadlineAt: Math.max(\n earliestDeadlineAt,\n maintenanceNotBeforeAt,\n ),\n };\n }\n\n return {\n dueRevalidations: dueRevalidations.sort(compareByInsertionOrder),\n dueRetransform,\n earliestDeadlineAt,\n };\n }\n\n #removeConnection(\n selector: ConnectionSelector,\n revision?: number,\n ): Readonly<ConnectionContext> | undefined {\n const connection = this.#getConnectionContext(selector);\n\n if (!connection) {\n return undefined;\n }\n\n // If the revision has changed, we should not remove the connection\n if (revision !== undefined && connection.revision !== revision) {\n this.#lc.debug?.('Ignoring failConnection for stale revision', {\n clientID: selector.clientID,\n wsID: selector.wsID,\n attemptedRevision: revision,\n currentRevision: connection.revision,\n });\n return undefined;\n }\n\n this.#connections.delete(connection.clientID);\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n\n return connection;\n }\n\n #demoteConnection(connection: ConnectionContext): ConnectionContext {\n const demotedConnection = this.#storeConnection({\n ...connection,\n state: 'provisional',\n revalidateAt: undefined,\n });\n this.#refreshBackgroundConnectionContext();\n this.#updateBackgroundRetransformDeadline(false);\n return demotedConnection;\n }\n\n /**\n * Keeps the background connection sticky while it remains validated.\n *\n * When a newly validated `preferred` connection is provided, it is promoted\n * only if there is no current validated background connection. Otherwise the\n * existing background connection stays in place until it disappears or is\n * demoted, at which point the newest validated connection is selected.\n */\n #refreshBackgroundConnectionContext(preferred?: ConnectionContext): void {\n if (preferred?.state === 'validated') {\n const currentBackgroundConnection =\n this.#getBackgroundConnectionContext();\n if (\n currentBackgroundConnection?.clientID === preferred.clientID &&\n currentBackgroundConnection.wsID === preferred.wsID\n ) {\n return;\n }\n if (currentBackgroundConnection !== undefined) {\n return;\n }\n this.#setBackgroundConnection({\n clientID: preferred.clientID,\n wsID: preferred.wsID,\n });\n this.#lc.debug?.('Selected background connection for shared auth work', {\n clientID: preferred.clientID,\n wsID: preferred.wsID,\n revision: preferred.revision,\n reason: 'preferred-validated',\n });\n return;\n }\n\n const currentBackgroundConnection = this.#getBackgroundConnectionContext();\n if (currentBackgroundConnection?.state === 'validated') {\n return;\n }\n\n const nextBackgroundConnection = [...this.#connections.values()]\n .filter(connection => connection.state === 'validated')\n .sort(comparePreferredValidatedConnection)\n .at(0);\n this.#setBackgroundConnection(\n nextBackgroundConnection\n ? {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n }\n : undefined,\n );\n if (nextBackgroundConnection) {\n this.#lc.debug?.('Selected background connection for shared auth work', {\n clientID: nextBackgroundConnection.clientID,\n wsID: nextBackgroundConnection.wsID,\n revision: nextBackgroundConnection.revision,\n reason: 'fallback-validated',\n });\n }\n }\n\n #getBackgroundConnectionContext(): ConnectionContext | undefined {\n const backgroundConnection = this.#group.backgroundConnection;\n if (!backgroundConnection) {\n return undefined;\n }\n return this.#getConnectionContext(backgroundConnection);\n }\n\n #getConnectionContext(\n selector: ConnectionSelector,\n ): ConnectionContext | undefined {\n const connection = this.#connections.get(selector.clientID);\n if (!connection) {\n return undefined;\n }\n if (connection.wsID !== selector.wsID) {\n return undefined;\n }\n return connection;\n }\n\n #mustGetConnectionContext(selector: ConnectionSelector): ConnectionContext {\n const connection = this.#getConnectionContext(selector);\n\n if (!connection) {\n throw new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.InvalidConnectionRequest,\n message:\n 'Connection auth state was not available for this websocket.',\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n\n return connection;\n }\n\n #storeConnection(connection: ConnectionContext): ConnectionContext {\n this.#connections.set(connection.clientID, connection);\n return connection;\n }\n\n #setGroup(group: GroupAuthState): GroupAuthState {\n this.#group = group;\n return group;\n }\n\n #setBackgroundConnection(\n backgroundConnection: ConnectionSelector | undefined,\n ) {\n if (\n sameConnectionSelector(\n this.#group.backgroundConnection,\n backgroundConnection,\n )\n ) {\n return;\n }\n this.#setGroup({\n ...this.#group,\n backgroundConnection: backgroundConnection\n ? {...backgroundConnection}\n : undefined,\n });\n }\n\n /**\n * Keeps the group background retransform deadline coherent with current\n * schedulability.\n *\n * When `reset` is false, this seeds a deadline only when shared retransform\n * is now possible and no deadline exists yet, preserving any existing\n * cadence. When `reset` is true, it starts a fresh interval from `#now()` if\n * retransform is schedulable for the current ready ViewSyncer instance, or\n * clears the deadline if it is not.\n */\n #updateBackgroundRetransformDeadline(reset: boolean) {\n const backgroundConnection = this.#getBackgroundConnectionContext();\n if (\n !backgroundConnection ||\n this.#retransformIntervalMs === undefined ||\n !this.#sharedRetransformReady\n ) {\n if (this.#group.retransformAt !== undefined) {\n this.#setGroup({\n ...this.#group,\n retransformAt: undefined,\n });\n }\n return;\n }\n\n if (reset || this.#group.retransformAt === undefined) {\n this.#setGroup({\n ...this.#group,\n retransformAt: this.#now() + this.#retransformIntervalMs,\n });\n }\n }\n\n #nextRevalidateAt() {\n return this.#revalidateIntervalMs === undefined\n ? undefined\n : this.#now() + this.#revalidateIntervalMs;\n }\n}\n\nfunction compareByInsertionOrder(\n a: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n b: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n) {\n return a.insertionOrder - b.insertionOrder || a.wsID.localeCompare(b.wsID);\n}\n\nfunction comparePreferredValidatedConnection(\n a: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n b: Pick<ConnectionContext, 'insertionOrder' | 'wsID'>,\n) {\n return b.insertionOrder - a.insertionOrder || b.wsID.localeCompare(a.wsID);\n}\n\nfunction minDefined(a: number | undefined, b: number | undefined) {\n if (a === undefined) {\n return b;\n }\n if (b === undefined) {\n return a;\n }\n return Math.min(a, b);\n}\n\nfunction sameConnectionSelector(\n a: ConnectionSelector | undefined,\n b: ConnectionSelector | undefined,\n) {\n return a?.clientID === b?.clientID && a?.wsID === b?.wsID;\n}\n\nfunction filterHeaders(\n headers: Readonly<Record<string, string>> | undefined,\n allowedHeaders: readonly string[] | undefined,\n) {\n if (!headers || !allowedHeaders || allowedHeaders.length === 0) {\n return undefined;\n }\n\n const allowed = new Set(allowedHeaders.map(header => header.toLowerCase()));\n let filtered: Record<string, string> | undefined;\n for (const [key, value] of Object.entries(headers)) {\n if (allowed.has(key.toLowerCase())) {\n filtered ??= {};\n filtered[key] = value;\n }\n }\n return filtered;\n}\n"],"mappings":"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"}
@@ -52,7 +52,7 @@ export declare class CVRStore {
52
52
  * `undefined`.
53
53
  */
54
54
  getTTLClock(): Promise<TTLClock | undefined>;
55
- putInstance({ version, replicaVersion, lastActive, clientSchema, profileID, ttlClock, }: Pick<CVRSnapshot, 'version' | 'replicaVersion' | 'lastActive' | 'clientSchema' | 'profileID' | 'ttlClock'>): void;
55
+ putInstance({ version, replicaVersion, lastActive, clientSchema, profileID, pinnedUser, ttlClock, }: Pick<CVRSnapshot, 'version' | 'replicaVersion' | 'lastActive' | 'clientSchema' | 'profileID' | 'pinnedUser' | 'ttlClock'>): void;
56
56
  markQueryAsDeleted(version: CVRVersion, queryPatch: QueryPatch): void;
57
57
  putQuery(query: QueryRecord): void;
58
58
  updateQuery(query: QueryRecord): void;
@@ -109,7 +109,15 @@ export declare class ClientNotFoundError extends ProtocolErrorWithLevel {
109
109
  }
110
110
  export declare class ConcurrentModificationException extends ProtocolErrorWithLevel {
111
111
  readonly name = "ConcurrentModificationException";
112
- constructor(expectedVersion: string, actualVersion: string);
112
+ constructor(detail: string);
113
+ }
114
+ /**
115
+ * The CVR was pinned to a different user than the one the flushing
116
+ * view-syncer validated, so that view-syncer's connections can no longer be
117
+ * trusted: it should stop and let its clients reconnect.
118
+ */
119
+ export declare class PinnedUserMismatchError extends ConcurrentModificationException {
120
+ constructor();
113
121
  }
114
122
  export declare class OwnershipError extends ProtocolErrorWithLevel {
115
123
  readonly name = "OwnershipError";
@@ -1 +1 @@
1
- {"version":3,"file":"cvr-store.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/cvr-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAkBjD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,+CAA+C,CAAC;AAUnF,OAAO,EAAC,sBAAsB,EAAC,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAC,UAAU,EAAE,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAEvE,OAAO,EAAY,KAAK,OAAO,EAAC,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAQ,cAAc,EAAC,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAC,GAAG,EAAE,WAAW,EAAC,MAAM,UAAU,CAAC;AAG/C,OAAO,EAKL,KAAK,OAAO,EACb,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAEL,KAAK,YAAY,EAGjB,KAAK,UAAU,EAGf,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,WAAW,EAEhB,KAAK,KAAK,EACV,KAAK,SAAS,EAGf,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,QAAQ,EAGd,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAkHF,qBAAa,QAAQ;;IAqEnB,YACE,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,EACjC,qBAAqB,SAA2B,EAChD,eAAe,SAAoB,EACnC,yBAAyB,SAAM,EAAE,qBAAqB;IACtD,YAAY,oBAAa,EAkB1B;IAiBD,IAAI,CAAC,EAAE,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAgC1D;IA+ND,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAEtD;IAED,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAEjC;IAED;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,EAAE,KAAK,GAAG,IAAI,CAE5B;IAED;;;;OAIG;IACH,YAAY,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,QAI3B;IAED;;;;OAIG;IACG,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK1E;IAED;;;;;OAKG;IACG,WAAW,IAAI,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAajD;IAED,WAAW,CAAC,EACV,OAAO,EACP,cAAc,EACd,UAAU,EACV,YAAY,EACZ,SAAS,EACT,QAAQ,GACT,EAAE,IAAI,CACL,WAAW,EACT,SAAS,GACT,gBAAgB,GAChB,YAAY,GACZ,cAAc,GACd,WAAW,GACX,UAAU,CACb,GAAG,IAAI,CAmBP;IAED,kBAAkB,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI,CASpE;IAED,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAqBjC;IAED,WAAW,CAAC,KAAK,EAAE,WAAW,QAY7B;IAED;;;;;OAKG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAErD;IAED,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAKhE;IAED,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAUvC;IAED,YAAY,CAAC,QAAQ,EAAE,MAAM,QAQ5B;IAED,eAAe,CACb,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,EACnB,MAAM,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,EACpB,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,QAAQ,GAAG,SAAS,EACnC,GAAG,EAAE,MAAM,GACV,IAAI,CAgBN;IAED,iBAAiB,CACf,EAAE,EAAE,UAAU,EACd,YAAY,EAAE,kBAAkB,EAChC,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,UAAU,EACnB,kBAAkB,GAAE,MAAM,EAAO,GAChC,cAAc,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,CAQ5C;IAEK,oBAAoB,CACxB,EAAE,EAAE,UAAU,EACd,YAAY,EAAE,kBAAkB,EAChC,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,cAAc,EAAE,CAAC,CAiE3B;IAwdD,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;;;;;OAMG;IACG,KAAK,CACT,EAAE,EAAE,UAAU,EACd,sBAAsB,EAAE,UAAU,EAClC,GAAG,EAAE,WAAW,EAChB,eAAe,EAAE,MAAM,EACvB,UAAU,UAAQ,GACjB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAwC/B;IAED;;;;;;;;;OASG;IACH,cAAc,IAAI,IAAI,CASrB;IAED,iBAAiB,IAAI,OAAO,CAE3B;IAED,qDAAqD;IACrD,OAAO,CAAC,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAErC;IAEK,cAAc,CAClB,EAAE,EAAE,UAAU,EACd,QAAQ,EAAE,QAAQ,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,EAAE,CAAC,CAqC5B;CACF;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,mBAAmB,EACvB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,sBAAsB,EAAE,UAAU,GACjC,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,qBAAa,mBAAoB,SAAQ,sBAAsB;IAC7D,YAAY,OAAO,EAAE,MAAM,EAS1B;CACF;AAED,qBAAa,+BAAgC,SAAQ,sBAAsB;IACzE,QAAQ,CAAC,IAAI,qCAAqC;IAElD,YAAY,eAAe,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EASzD;CACF;AAED,qBAAa,cAAe,SAAQ,sBAAsB;IACxD,QAAQ,CAAC,IAAI,oBAAoB;IAEjC,YACE,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,eAAe,EAAE,MAAM,EAcxB;CACF;AAED,qBAAa,wBAAyB,SAAQ,sBAAsB;IAClE,QAAQ,CAAC,IAAI,8BAA8B;IAE3C,YAAY,KAAK,EAAE,OAAO,EAUzB;CACF;AAkBD,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,4BAA4B;IACzC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAEpC,YAAY,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,EAIzD;CACF"}
1
+ {"version":3,"file":"cvr-store.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/cvr-store.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { assert } from "../../../../shared/src/asserts.js";
2
2
  import { deepEqual } from "../../../../shared/src/json.js";
3
3
  import { sleep } from "../../../../shared/src/sleep.js";
4
- import { parse } from "../../../../shared/src/valita.js";
4
+ import { parse, test, valita_exports } from "../../../../shared/src/valita.js";
5
5
  import { some } from "../../../../shared/src/iterables.js";
6
6
  import { astSchema } from "../../../../zero-protocol/src/ast.js";
7
7
  import { ClientNotFound, Rehome, SchemaVersionNotSupported } from "../../../../zero-protocol/src/error-kind-enum.js";
@@ -27,6 +27,7 @@ import { CustomKeySet } from "../../../../shared/src/custom-key-set.js";
27
27
  import { RowRecordCache } from "./row-record-cache.js";
28
28
  import { trace } from "@opentelemetry/api";
29
29
  //#region ../zero-cache/src/services/view-syncer/cvr-store.ts
30
+ var pinnedUserSchema = valita_exports.object({ id: valita_exports.string().nullable() });
30
31
  var flushCounter = 0;
31
32
  var RESULT_ATTRIBUTE = "result";
32
33
  var ERROR_KIND_ATTRIBUTE = "error.kind";
@@ -70,7 +71,9 @@ function asQuery(row) {
70
71
  transformationVersion: maybeVersion(row.transformationVersion)
71
72
  };
72
73
  }
73
- const ast = astSchema.parse(row.clientAST);
74
+ const parsed = test(row.clientAST, astSchema);
75
+ if (!parsed.ok) throw new InvalidClientASTError(row.queryHash, parsed.error);
76
+ const ast = parsed.value;
74
77
  return row.internal ? {
75
78
  type: "internal",
76
79
  id: row.queryHash,
@@ -191,7 +194,8 @@ var CVRStore = class {
191
194
  clients: {},
192
195
  queries: {},
193
196
  clientSchema: null,
194
- profileID: null
197
+ profileID: null,
198
+ pinnedUser: null
195
199
  };
196
200
  const [instance, clientsRows, queryRows, desiresRows] = await runTx(this.#db, (tx) => {
197
201
  lc.debug?.(`CVR tx started after ${Date.now() - start} ms`);
@@ -204,6 +208,7 @@ var CVRStore = class {
204
208
  "grantedAt",
205
209
  "clientSchema",
206
210
  "profileID",
211
+ "pinnedUser",
207
212
  "deleted",
208
213
  rows."version" as "rowsVersion"
209
214
  FROM ${this.#cvr("instances")} AS cvr
@@ -245,11 +250,12 @@ var CVRStore = class {
245
250
  ttlClock: ttlClockFromNumber(0),
246
251
  replicaVersion: null,
247
252
  clientSchema: null,
248
- profileID: null
253
+ profileID: null,
254
+ pinnedUser: null
249
255
  });
250
256
  else {
251
257
  assert(instance.length === 1, () => `Expected exactly one CVR instance, got ${instance.length}`);
252
- const { version, lastActive, ttlClock, replicaVersion, owner, grantedAt, rowsVersion, clientSchema, profileID, deleted } = instance[0];
258
+ const { version, lastActive, ttlClock, replicaVersion, owner, grantedAt, rowsVersion, clientSchema, profileID, pinnedUser, deleted } = instance[0];
253
259
  if (deleted) throw new ClientNotFoundError("Client has been purged due to inactivity");
254
260
  if (owner !== this.#taskID) {
255
261
  if ((grantedAt ?? 0) > lastConnectTime) throw new OwnershipError(owner, grantedAt, lastConnectTime);
@@ -268,6 +274,7 @@ var CVRStore = class {
268
274
  cvr.ttlClock = ttlClock;
269
275
  cvr.replicaVersion = replicaVersion;
270
276
  cvr.profileID = profileID;
277
+ cvr.pinnedUser = pinnedUser === null ? null : parse(pinnedUser, pinnedUserSchema);
271
278
  try {
272
279
  cvr.clientSchema = clientSchema === null ? null : parse(clientSchema, clientSchemaSchema, "strip");
273
280
  } catch (e) {
@@ -350,7 +357,7 @@ var CVRStore = class {
350
357
  assert(result.length === 1, () => `Expected exactly one rowsVersion result, got ${result.length}`);
351
358
  return result[0][0];
352
359
  }
353
- putInstance({ version, replicaVersion, lastActive, clientSchema, profileID, ttlClock }) {
360
+ putInstance({ version, replicaVersion, lastActive, clientSchema, profileID, pinnedUser, ttlClock }) {
354
361
  this.#pendingInstanceWrite = (tx, lastConnectTime) => {
355
362
  const change = {
356
363
  clientGroupID: this.#id,
@@ -361,7 +368,8 @@ var CVRStore = class {
361
368
  owner: this.#taskID,
362
369
  grantedAt: lastConnectTime,
363
370
  clientSchema,
364
- profileID
371
+ profileID,
372
+ pinnedUser
365
373
  };
366
374
  return tx`
367
375
  INSERT INTO ${this.#cvr("instances")} ${tx(change)}
@@ -706,7 +714,13 @@ var CVRStore = class {
706
714
  "inactivatedAtMs" = excluded."inactivatedAtMs"
707
715
  `;
708
716
  }
709
- async #checkVersionAndOwnership(lc, tx, expectedCurrentVersion, lastConnectTime) {
717
+ /**
718
+ * Also checks that the CVR has not been pinned to a user other than
719
+ * `pinnedUser` since it was loaded: the first pin does not change the CVR
720
+ * version, so the version check alone would let two view-syncers that both
721
+ * loaded an unpinned CVR pin it to different users in turn.
722
+ */
723
+ async #checkVersionAndOwnership(lc, tx, expectedCurrentVersion, lastConnectTime, pinnedUser) {
710
724
  const start = Date.now();
711
725
  lc.debug?.("checking cvr version and ownership");
712
726
  const expected = versionString(expectedCurrentVersion);
@@ -723,17 +737,19 @@ var CVRStore = class {
723
737
  )
724
738
  ON CONFLICT ("clientGroupID") DO NOTHING
725
739
  `;
726
- const result = await tx`SELECT "version", "owner", "grantedAt" FROM ${this.#cvr("instances")}
740
+ const result = await tx`SELECT "version", "owner", "grantedAt", "pinnedUser"
741
+ FROM ${this.#cvr("instances")}
727
742
  WHERE "clientGroupID" = ${this.#id}
728
743
  FOR UPDATE`;
729
- const { version, owner, grantedAt } = result.length > 0 ? result[0] : {
744
+ const { version, owner, grantedAt, pinnedUser: storedPinnedUser = null } = result.length > 0 ? result[0] : {
730
745
  version: EMPTY_CVR_VERSION.stateVersion,
731
746
  owner: null,
732
747
  grantedAt: null
733
748
  };
734
749
  lc.debug?.("checked cvr version and ownership in " + (Date.now() - start) + " ms");
735
750
  if (owner !== this.#taskID && (grantedAt ?? 0) > lastConnectTime) throw new OwnershipError(owner, grantedAt, lastConnectTime);
736
- if (version !== expected) throw new ConcurrentModificationException(expected, version);
751
+ if (storedPinnedUser !== null && storedPinnedUser.id !== pinnedUser?.id) throw new PinnedUserMismatchError();
752
+ if (version !== expected) throw new ConcurrentModificationException(`Expected ${expected}, got ${version}`);
737
753
  }
738
754
  async #flush(lc, expectedCurrentVersion, cvr, lastConnectTime, verifyNoop) {
739
755
  const stats = {
@@ -755,7 +771,7 @@ var CVRStore = class {
755
771
  }
756
772
  }
757
773
  if (this.#pendingRowRecordUpdates.size === 0 && this.#writes.size === 0 && this.#pendingInstanceWrite === void 0 && this.#pendingQueryUpdates.size === 0 && this.#pendingQueryPartialUpdates.size === 0 && this.#pendingDesireUpdates.size === 0) {
758
- if (verifyNoop) await runTx(this.#db, (tx) => this.#checkVersionAndOwnership(lc, tx, expectedCurrentVersion, lastConnectTime), { mode: READ_COMMITTED });
774
+ if (verifyNoop) await runTx(this.#db, (tx) => this.#checkVersionAndOwnership(lc, tx, expectedCurrentVersion, lastConnectTime, cvr.pinnedUser), { mode: READ_COMMITTED });
759
775
  return null;
760
776
  }
761
777
  this.putInstance(cvr);
@@ -763,7 +779,7 @@ var CVRStore = class {
763
779
  lc.debug?.("flush tx beginning");
764
780
  const results = await runTx(this.#db, async (tx) => {
765
781
  lc.debug?.(`flush tx begun after ${Date.now() - start} ms`);
766
- await this.#checkVersionAndOwnership(lc, tx, expectedCurrentVersion, lastConnectTime);
782
+ await this.#checkVersionAndOwnership(lc, tx, expectedCurrentVersion, lastConnectTime, cvr.pinnedUser);
767
783
  const writeQueries = [];
768
784
  if (this.#pendingInstanceWrite) {
769
785
  writeQueries.push(this.#pendingInstanceWrite(tx, lastConnectTime));
@@ -924,7 +940,7 @@ async function checkVersion(tx, schema, clientGroupID, expectedCurrentVersion) {
924
940
  SELECT version FROM ${tx(schema)}.instances
925
941
  WHERE "clientGroupID" = ${clientGroupID}`;
926
942
  const { version } = result.length > 0 ? result[0] : { version: EMPTY_CVR_VERSION.stateVersion };
927
- if (version !== expected) throw new ConcurrentModificationException(expected, version);
943
+ if (version !== expected) throw new ConcurrentModificationException(`Expected ${expected}, got ${version}`);
928
944
  }
929
945
  var ClientNotFoundError = class extends ProtocolErrorWithLevel {
930
946
  constructor(message) {
@@ -937,14 +953,24 @@ var ClientNotFoundError = class extends ProtocolErrorWithLevel {
937
953
  };
938
954
  var ConcurrentModificationException = class extends ProtocolErrorWithLevel {
939
955
  name = "ConcurrentModificationException";
940
- constructor(expectedVersion, actualVersion) {
956
+ constructor(detail) {
941
957
  super({
942
958
  kind: Rehome,
943
- message: `CVR has been concurrently modified. Expected ${expectedVersion}, got ${actualVersion}`,
959
+ message: `CVR has been concurrently modified. ${detail}`,
944
960
  origin: ZeroCache
945
961
  }, "warn");
946
962
  }
947
963
  };
964
+ /**
965
+ * The CVR was pinned to a different user than the one the flushing
966
+ * view-syncer validated, so that view-syncer's connections can no longer be
967
+ * trusted: it should stop and let its clients reconnect.
968
+ */
969
+ var PinnedUserMismatchError = class extends ConcurrentModificationException {
970
+ constructor() {
971
+ super("Client group was pinned to a different user");
972
+ }
973
+ };
948
974
  var OwnershipError = class extends ProtocolErrorWithLevel {
949
975
  name = "OwnershipError";
950
976
  constructor(owner, grantedAt, lastConnectTime) {
@@ -956,6 +982,16 @@ var OwnershipError = class extends ProtocolErrorWithLevel {
956
982
  }, "info");
957
983
  }
958
984
  };
985
+ var InvalidClientASTError = class extends ProtocolErrorWithLevel {
986
+ name = "InvalidClientASTError";
987
+ constructor(queryHash, error) {
988
+ super({
989
+ kind: SchemaVersionNotSupported,
990
+ message: `Could not parse AST of query ${queryHash} stored in CVR: ${error}`,
991
+ origin: ZeroCache
992
+ }, "warn");
993
+ }
994
+ };
959
995
  var InvalidClientSchemaError = class extends ProtocolErrorWithLevel {
960
996
  name = "InvalidClientSchemaError";
961
997
  constructor(cause) {
@@ -971,6 +1007,7 @@ function cvrErrorKind(e) {
971
1007
  if (e instanceof ConcurrentModificationException) return "concurrent_modification";
972
1008
  if (e instanceof OwnershipError) return "ownership";
973
1009
  if (e instanceof InvalidClientSchemaError) return "invalid_client_schema";
1010
+ if (e instanceof InvalidClientASTError) return "invalid_client_ast";
974
1011
  return "error";
975
1012
  }
976
1013
  var RowsVersionBehindError = class extends Error {
@@ -984,6 +1021,6 @@ var RowsVersionBehindError = class extends Error {
984
1021
  }
985
1022
  };
986
1023
  //#endregion
987
- export { CVRStore, ClientNotFoundError, ConcurrentModificationException, InvalidClientSchemaError, OwnershipError, RowsVersionBehindError, checkVersion };
1024
+ export { CVRStore, ClientNotFoundError, ConcurrentModificationException, InvalidClientSchemaError, OwnershipError, PinnedUserMismatchError, RowsVersionBehindError, checkVersion };
988
1025
 
989
1026
  //# sourceMappingURL=cvr-store.js.map