@segmentstream/ontology 0.0.0-stage → 0.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (483) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +2 -0
  3. package/README.md +1655 -2
  4. package/dist/action.d.ts +193 -0
  5. package/dist/action.js +41 -0
  6. package/dist/agent-chat.d.ts +44 -0
  7. package/dist/agent-chat.js +91 -0
  8. package/dist/agent-task-contract.d.ts +64 -0
  9. package/dist/agent-task-contract.js +15 -0
  10. package/dist/agent-task.d.ts +39 -0
  11. package/dist/agent-task.js +112 -0
  12. package/dist/analyze.d.ts +215 -0
  13. package/dist/analyze.js +2438 -0
  14. package/dist/app-feedback.d.ts +167 -0
  15. package/dist/app-feedback.js +176 -0
  16. package/dist/authoring-input.d.ts +3 -0
  17. package/dist/authoring-input.js +107 -0
  18. package/dist/authoring-names.d.ts +4 -0
  19. package/dist/authoring-names.js +13 -0
  20. package/dist/authoring-records.d.ts +35 -0
  21. package/dist/authoring-records.js +264 -0
  22. package/dist/authoring.d.ts +15 -0
  23. package/dist/authoring.js +331 -0
  24. package/dist/bin/check.d.ts +30 -0
  25. package/dist/bin/check.js +99 -0
  26. package/dist/bin/load.d.ts +48 -0
  27. package/dist/bin/load.js +139 -0
  28. package/dist/bin/main.d.ts +31 -0
  29. package/dist/bin/main.js +121 -0
  30. package/dist/bin/migrate.d.ts +57 -0
  31. package/dist/bin/migrate.js +170 -0
  32. package/dist/bin/output.d.ts +31 -0
  33. package/dist/bin/output.js +84 -0
  34. package/dist/bin.d.ts +2 -0
  35. package/dist/bin.js +31 -0
  36. package/dist/blob.d.ts +52 -0
  37. package/dist/blob.js +87 -0
  38. package/dist/channel-session.d.ts +16 -0
  39. package/dist/channel-session.js +16 -0
  40. package/dist/chat.d.ts +43 -0
  41. package/dist/chat.js +34 -0
  42. package/dist/confirmation.d.ts +46 -0
  43. package/dist/confirmation.js +41 -0
  44. package/dist/connection.d.ts +213 -0
  45. package/dist/connection.js +289 -0
  46. package/dist/deployment.d.ts +35 -0
  47. package/dist/deployment.js +76 -0
  48. package/dist/diagnostics.d.ts +27 -0
  49. package/dist/diagnostics.js +106 -0
  50. package/dist/emails.d.ts +70 -0
  51. package/dist/emails.js +221 -0
  52. package/dist/entity.d.ts +331 -0
  53. package/dist/entity.js +153 -0
  54. package/dist/errors.d.ts +154 -0
  55. package/dist/errors.js +109 -0
  56. package/dist/feedback-config.d.ts +24 -0
  57. package/dist/feedback-config.js +126 -0
  58. package/dist/files.d.ts +39 -0
  59. package/dist/files.js +4 -0
  60. package/dist/groups.d.ts +95 -0
  61. package/dist/groups.js +280 -0
  62. package/dist/index.d.ts +109 -0
  63. package/dist/index.js +70 -0
  64. package/dist/internal/diagnostics.js +594 -0
  65. package/dist/legacy.d.ts +160 -0
  66. package/dist/legacy.js +270 -0
  67. package/dist/manifest-compatibility.d.ts +8 -0
  68. package/dist/manifest-compatibility.js +87 -0
  69. package/dist/manifest.d.ts +3329 -0
  70. package/dist/manifest.js +757 -0
  71. package/dist/me.d.ts +46 -0
  72. package/dist/me.js +109 -0
  73. package/dist/metric-filters.d.ts +48 -0
  74. package/dist/metric-filters.js +252 -0
  75. package/dist/metric-routes.d.ts +132 -0
  76. package/dist/metric-routes.js +498 -0
  77. package/dist/metric.d.ts +172 -0
  78. package/dist/metric.js +132 -0
  79. package/dist/migrate/apply.d.ts +94 -0
  80. package/dist/migrate/apply.js +526 -0
  81. package/dist/migrate/errors.d.ts +46 -0
  82. package/dist/migrate/errors.js +38 -0
  83. package/dist/migrate/index.d.ts +10 -0
  84. package/dist/migrate/index.js +7 -0
  85. package/dist/migrate/model.d.ts +2311 -0
  86. package/dist/migrate/model.js +148 -0
  87. package/dist/migrate/plan.d.ts +51 -0
  88. package/dist/migrate/plan.js +1856 -0
  89. package/dist/migrate/preview.d.ts +13 -0
  90. package/dist/migrate/preview.js +65 -0
  91. package/dist/migrate/storage.d.ts +226 -0
  92. package/dist/migrate/storage.js +774 -0
  93. package/dist/model-metrics.d.ts +22 -0
  94. package/dist/model-metrics.js +545 -0
  95. package/dist/names.d.ts +49 -0
  96. package/dist/names.js +110 -0
  97. package/dist/navigation-artifact.d.ts +13 -0
  98. package/dist/navigation-artifact.js +11 -0
  99. package/dist/navigation-catalog.d.ts +55 -0
  100. package/dist/navigation-catalog.js +297 -0
  101. package/dist/nitro-navigation.d.ts +3 -0
  102. package/dist/nitro-navigation.js +64 -0
  103. package/dist/nitro-workflows.d.ts +108 -0
  104. package/dist/nitro-workflows.js +286 -0
  105. package/dist/nitro.d.ts +40 -0
  106. package/dist/nitro.js +50 -0
  107. package/dist/ontology.d.ts +41 -0
  108. package/dist/ontology.js +46 -0
  109. package/dist/permissions.d.ts +198 -0
  110. package/dist/permissions.js +272 -0
  111. package/dist/property.d.ts +188 -0
  112. package/dist/property.js +301 -0
  113. package/dist/query.d.ts +245 -0
  114. package/dist/query.js +615 -0
  115. package/dist/relationship.d.ts +99 -0
  116. package/dist/relationship.js +33 -0
  117. package/dist/render.d.ts +81 -0
  118. package/dist/render.js +512 -0
  119. package/dist/replication/baseline.d.ts +51 -0
  120. package/dist/replication/baseline.js +215 -0
  121. package/dist/replication/bigquery.d.ts +37 -0
  122. package/dist/replication/bigquery.js +211 -0
  123. package/dist/replication/coverage.d.ts +14 -0
  124. package/dist/replication/coverage.js +53 -0
  125. package/dist/replication/evolution.d.ts +25 -0
  126. package/dist/replication/evolution.js +184 -0
  127. package/dist/replication/identities.d.ts +6 -0
  128. package/dist/replication/identities.js +47 -0
  129. package/dist/replication/legacy-baseline.d.ts +27 -0
  130. package/dist/replication/legacy-baseline.js +109 -0
  131. package/dist/replication/legacy-store.d.ts +19 -0
  132. package/dist/replication/legacy-store.js +63 -0
  133. package/dist/replication/lineage.d.ts +36 -0
  134. package/dist/replication/lineage.js +72 -0
  135. package/dist/replication/migrate.d.ts +21 -0
  136. package/dist/replication/migrate.js +84 -0
  137. package/dist/replication/model.d.ts +52 -0
  138. package/dist/replication/model.js +99 -0
  139. package/dist/replication/scans.d.ts +34 -0
  140. package/dist/replication/scans.js +158 -0
  141. package/dist/replication/schema.d.ts +11 -0
  142. package/dist/replication/schema.js +226 -0
  143. package/dist/replication/shadow-schema.d.ts +4 -0
  144. package/dist/replication/shadow-schema.js +61 -0
  145. package/dist/replication/shadow.d.ts +127 -0
  146. package/dist/replication/shadow.js +614 -0
  147. package/dist/replication/source-replacement.d.ts +33 -0
  148. package/dist/replication/source-replacement.js +76 -0
  149. package/dist/replication/store.d.ts +36 -0
  150. package/dist/replication/store.js +382 -0
  151. package/dist/replication/transaction.d.ts +23 -0
  152. package/dist/replication/transaction.js +88 -0
  153. package/dist/role.d.ts +61 -0
  154. package/dist/role.js +76 -0
  155. package/dist/runtime/action-deadlocks.d.ts +21 -0
  156. package/dist/runtime/action-deadlocks.js +130 -0
  157. package/dist/runtime/action-transactions.d.ts +35 -0
  158. package/dist/runtime/action-transactions.js +54 -0
  159. package/dist/runtime/agent-counters.d.ts +26 -0
  160. package/dist/runtime/agent-counters.js +98 -0
  161. package/dist/runtime/agent-tasks.d.ts +63 -0
  162. package/dist/runtime/agent-tasks.js +120 -0
  163. package/dist/runtime/agents.d.ts +15 -0
  164. package/dist/runtime/agents.js +457 -0
  165. package/dist/runtime/batch-diagnostics.d.ts +22 -0
  166. package/dist/runtime/batch-diagnostics.js +89 -0
  167. package/dist/runtime/batch.d.ts +52 -0
  168. package/dist/runtime/batch.js +534 -0
  169. package/dist/runtime/channel-session-revocation.d.ts +3 -0
  170. package/dist/runtime/channel-session-revocation.js +5 -0
  171. package/dist/runtime/channel-sessions.d.ts +4 -0
  172. package/dist/runtime/channel-sessions.js +61 -0
  173. package/dist/runtime/confirmations.d.ts +29 -0
  174. package/dist/runtime/confirmations.js +57 -0
  175. package/dist/runtime/cursor.d.ts +23 -0
  176. package/dist/runtime/cursor.js +100 -0
  177. package/dist/runtime/deployment.d.ts +7 -0
  178. package/dist/runtime/deployment.js +4 -0
  179. package/dist/runtime/errors.d.ts +118 -0
  180. package/dist/runtime/errors.js +109 -0
  181. package/dist/runtime/federation-tables.d.ts +2 -0
  182. package/dist/runtime/federation-tables.js +20 -0
  183. package/dist/runtime/federation.d.ts +42 -0
  184. package/dist/runtime/federation.js +222 -0
  185. package/dist/runtime/files.d.ts +39 -0
  186. package/dist/runtime/files.js +44 -0
  187. package/dist/runtime/generated-actions.d.ts +37 -0
  188. package/dist/runtime/generated-actions.js +307 -0
  189. package/dist/runtime/history.d.ts +91 -0
  190. package/dist/runtime/history.js +77 -0
  191. package/dist/runtime/ids.d.ts +6 -0
  192. package/dist/runtime/ids.js +39 -0
  193. package/dist/runtime/member-tables.d.ts +8 -0
  194. package/dist/runtime/member-tables.js +33 -0
  195. package/dist/runtime/member-writes.d.ts +78 -0
  196. package/dist/runtime/member-writes.js +202 -0
  197. package/dist/runtime/metric-query.d.ts +19 -0
  198. package/dist/runtime/metric-query.js +411 -0
  199. package/dist/runtime/model.d.ts +67 -0
  200. package/dist/runtime/model.js +150 -0
  201. package/dist/runtime/new-user-hook.d.ts +6 -0
  202. package/dist/runtime/new-user-hook.js +37 -0
  203. package/dist/runtime/oauth.d.ts +9 -0
  204. package/dist/runtime/oauth.js +299 -0
  205. package/dist/runtime/pipeline.d.ts +155 -0
  206. package/dist/runtime/pipeline.js +597 -0
  207. package/dist/runtime/postgres-tls.d.ts +10 -0
  208. package/dist/runtime/postgres-tls.js +14 -0
  209. package/dist/runtime/principals.d.ts +302 -0
  210. package/dist/runtime/principals.js +1307 -0
  211. package/dist/runtime/reads.d.ts +55 -0
  212. package/dist/runtime/reads.js +313 -0
  213. package/dist/runtime/records.d.ts +42 -0
  214. package/dist/runtime/records.js +245 -0
  215. package/dist/runtime/resolve-agent-identity.d.ts +6 -0
  216. package/dist/runtime/resolve-agent-identity.js +181 -0
  217. package/dist/runtime/service-binding.d.ts +18 -0
  218. package/dist/runtime/service-binding.js +266 -0
  219. package/dist/runtime/sign-in-links.d.ts +68 -0
  220. package/dist/runtime/sign-in-links.js +147 -0
  221. package/dist/runtime/sql-runtime.d.ts +131 -0
  222. package/dist/runtime/sql-runtime.js +362 -0
  223. package/dist/runtime/statement.d.ts +18 -0
  224. package/dist/runtime/statement.js +31 -0
  225. package/dist/runtime/user-identities.d.ts +66 -0
  226. package/dist/runtime/user-identities.js +106 -0
  227. package/dist/runtime/user-identity-sql.d.ts +86 -0
  228. package/dist/runtime/user-identity-sql.js +297 -0
  229. package/dist/runtime/user-roles.d.ts +16 -0
  230. package/dist/runtime/user-roles.js +32 -0
  231. package/dist/runtime/validation.d.ts +16 -0
  232. package/dist/runtime/validation.js +26 -0
  233. package/dist/runtime/where.d.ts +57 -0
  234. package/dist/runtime/where.js +404 -0
  235. package/dist/runtime/workflow-admission.d.ts +49 -0
  236. package/dist/runtime/workflow-admission.js +92 -0
  237. package/dist/runtime/workflow-argument-codec.d.ts +10 -0
  238. package/dist/runtime/workflow-argument-codec.js +120 -0
  239. package/dist/runtime/workflow-arguments.d.ts +17 -0
  240. package/dist/runtime/workflow-arguments.js +99 -0
  241. package/dist/runtime/workflow-starts.d.ts +203 -0
  242. package/dist/runtime/workflow-starts.js +493 -0
  243. package/dist/runtime/workflow-users.d.ts +9 -0
  244. package/dist/runtime/workflow-users.js +17 -0
  245. package/dist/runtime/workload.d.ts +25 -0
  246. package/dist/runtime/workload.js +24 -0
  247. package/dist/runtime/writes.d.ts +108 -0
  248. package/dist/runtime/writes.js +491 -0
  249. package/dist/runtime-source.d.ts +22 -0
  250. package/dist/runtime-source.js +28 -0
  251. package/dist/runtime.d.ts +310 -0
  252. package/dist/runtime.js +31 -0
  253. package/dist/server.d.ts +23 -0
  254. package/dist/server.js +107 -0
  255. package/dist/service/agent/attachments.d.ts +33 -0
  256. package/dist/service/agent/attachments.js +228 -0
  257. package/dist/service/agent/authentication.d.ts +22 -0
  258. package/dist/service/agent/authentication.js +58 -0
  259. package/dist/service/agent/confirmations.d.ts +5 -0
  260. package/dist/service/agent/confirmations.js +73 -0
  261. package/dist/service/agent/conversation-routes.d.ts +14 -0
  262. package/dist/service/agent/conversation-routes.js +45 -0
  263. package/dist/service/agent/conversations.d.ts +40 -0
  264. package/dist/service/agent/conversations.js +69 -0
  265. package/dist/service/agent/files.d.ts +12 -0
  266. package/dist/service/agent/files.js +69 -0
  267. package/dist/service/agent/protocol.d.ts +90 -0
  268. package/dist/service/agent/protocol.js +114 -0
  269. package/dist/service/agent/server.d.ts +28 -0
  270. package/dist/service/agent/server.js +374 -0
  271. package/dist/service/agent/task-caller.d.ts +21 -0
  272. package/dist/service/agent/task-caller.js +35 -0
  273. package/dist/service/agent/task-start.d.ts +53 -0
  274. package/dist/service/agent/task-start.js +161 -0
  275. package/dist/service/agent/tasks.d.ts +40 -0
  276. package/dist/service/agent/tasks.js +155 -0
  277. package/dist/service/api.d.ts +50 -0
  278. package/dist/service/api.js +288 -0
  279. package/dist/service/authentication.d.ts +227 -0
  280. package/dist/service/authentication.js +767 -0
  281. package/dist/service/caller-context.d.ts +24 -0
  282. package/dist/service/caller-context.js +6 -0
  283. package/dist/service/catalog.d.ts +42 -0
  284. package/dist/service/catalog.js +66 -0
  285. package/dist/service/channel/provider.d.ts +10 -0
  286. package/dist/service/channel/provider.js +1 -0
  287. package/dist/service/channel/providers/telegram.d.ts +18 -0
  288. package/dist/service/channel/providers/telegram.js +108 -0
  289. package/dist/service/channel/sessions.d.ts +5 -0
  290. package/dist/service/channel/sessions.js +53 -0
  291. package/dist/service/channel/sign-in.d.ts +7 -0
  292. package/dist/service/channel/sign-in.js +78 -0
  293. package/dist/service/chat/archive.d.ts +64 -0
  294. package/dist/service/chat/archive.js +62 -0
  295. package/dist/service/chat/attribution.d.ts +26 -0
  296. package/dist/service/chat/attribution.js +28 -0
  297. package/dist/service/chat/delivery.d.ts +18 -0
  298. package/dist/service/chat/delivery.js +251 -0
  299. package/dist/service/chat/diagnostics.d.ts +2 -0
  300. package/dist/service/chat/diagnostics.js +35 -0
  301. package/dist/service/chat/envelope.d.ts +96 -0
  302. package/dist/service/chat/envelope.js +39 -0
  303. package/dist/service/chat/idempotency.d.ts +19 -0
  304. package/dist/service/chat/idempotency.js +37 -0
  305. package/dist/service/chat/providers/telegram.d.ts +36 -0
  306. package/dist/service/chat/providers/telegram.js +133 -0
  307. package/dist/service/chat/reads.d.ts +25 -0
  308. package/dist/service/chat/reads.js +75 -0
  309. package/dist/service/chat/resolve.d.ts +34 -0
  310. package/dist/service/chat/resolve.js +150 -0
  311. package/dist/service/chat/routes.d.ts +23 -0
  312. package/dist/service/chat/routes.js +75 -0
  313. package/dist/service/chat/schemas.d.ts +43 -0
  314. package/dist/service/chat/schemas.js +31 -0
  315. package/dist/service/config.d.ts +218 -0
  316. package/dist/service/config.js +265 -0
  317. package/dist/service/connection-stream.d.ts +10 -0
  318. package/dist/service/connection-stream.js +191 -0
  319. package/dist/service/connections.d.ts +60 -0
  320. package/dist/service/connections.js +271 -0
  321. package/dist/service/decisions.d.ts +35 -0
  322. package/dist/service/decisions.js +63 -0
  323. package/dist/service/descriptions.d.ts +8 -0
  324. package/dist/service/descriptions.js +20 -0
  325. package/dist/service/discovery-schemas.d.ts +92 -0
  326. package/dist/service/discovery-schemas.js +59 -0
  327. package/dist/service/discovery-search.d.ts +30 -0
  328. package/dist/service/discovery-search.js +95 -0
  329. package/dist/service/discovery.d.ts +37 -0
  330. package/dist/service/discovery.js +436 -0
  331. package/dist/service/docs.d.ts +9 -0
  332. package/dist/service/docs.js +197 -0
  333. package/dist/service/errors.d.ts +145 -0
  334. package/dist/service/errors.js +704 -0
  335. package/dist/service/federation-provider.d.ts +36 -0
  336. package/dist/service/federation-provider.js +110 -0
  337. package/dist/service/federation-server.d.ts +20 -0
  338. package/dist/service/federation-server.js +249 -0
  339. package/dist/service/federation-status.d.ts +40 -0
  340. package/dist/service/federation-status.js +151 -0
  341. package/dist/service/handlers.d.ts +18 -0
  342. package/dist/service/handlers.js +287 -0
  343. package/dist/service/mcp/confirmation.d.ts +68 -0
  344. package/dist/service/mcp/confirmation.js +178 -0
  345. package/dist/service/mcp/find-input.d.ts +12 -0
  346. package/dist/service/mcp/find-input.js +122 -0
  347. package/dist/service/mcp/input-schema.d.ts +6 -0
  348. package/dist/service/mcp/input-schema.js +126 -0
  349. package/dist/service/mcp/listing.d.ts +26 -0
  350. package/dist/service/mcp/listing.js +92 -0
  351. package/dist/service/mcp/metric-tools.d.ts +85 -0
  352. package/dist/service/mcp/metric-tools.js +258 -0
  353. package/dist/service/mcp/request-log.d.ts +5 -0
  354. package/dist/service/mcp/request-log.js +53 -0
  355. package/dist/service/mcp/runtime-policy.d.ts +27 -0
  356. package/dist/service/mcp/runtime-policy.js +62 -0
  357. package/dist/service/mcp/server.d.ts +52 -0
  358. package/dist/service/mcp/server.js +861 -0
  359. package/dist/service/mcp/tools.d.ts +99 -0
  360. package/dist/service/mcp/tools.js +938 -0
  361. package/dist/service/member-file-routes.d.ts +22 -0
  362. package/dist/service/member-file-routes.js +52 -0
  363. package/dist/service/member-files.d.ts +245 -0
  364. package/dist/service/member-files.js +258 -0
  365. package/dist/service/metric-query-platform.d.ts +77 -0
  366. package/dist/service/metric-query-platform.js +198 -0
  367. package/dist/service/metric-query-scopes.d.ts +43 -0
  368. package/dist/service/metric-query-scopes.js +147 -0
  369. package/dist/service/metric-query.d.ts +48 -0
  370. package/dist/service/metric-query.js +200 -0
  371. package/dist/service/name-suggestions.d.ts +3 -0
  372. package/dist/service/name-suggestions.js +38 -0
  373. package/dist/service/navigation-schemas.d.ts +80 -0
  374. package/dist/service/navigation-schemas.js +49 -0
  375. package/dist/service/navigation.d.ts +20 -0
  376. package/dist/service/navigation.js +165 -0
  377. package/dist/service/oauth/protocol.d.ts +70 -0
  378. package/dist/service/oauth/protocol.js +157 -0
  379. package/dist/service/oauth/refresh-replay.d.ts +2 -0
  380. package/dist/service/oauth/refresh-replay.js +20 -0
  381. package/dist/service/oauth/server.d.ts +28 -0
  382. package/dist/service/oauth/server.js +769 -0
  383. package/dist/service/operations.d.ts +170 -0
  384. package/dist/service/operations.js +392 -0
  385. package/dist/service/plan.d.ts +57 -0
  386. package/dist/service/plan.js +96 -0
  387. package/dist/service/rate-limit.d.ts +22 -0
  388. package/dist/service/rate-limit.js +52 -0
  389. package/dist/service/record-reads.d.ts +21 -0
  390. package/dist/service/record-reads.js +155 -0
  391. package/dist/service/replication.d.ts +5 -0
  392. package/dist/service/replication.js +39 -0
  393. package/dist/service/runtime-contract.d.ts +508 -0
  394. package/dist/service/runtime-contract.js +18 -0
  395. package/dist/service/runtime-identity.d.ts +71 -0
  396. package/dist/service/runtime-identity.js +122 -0
  397. package/dist/service/schemas.d.ts +961 -0
  398. package/dist/service/schemas.js +639 -0
  399. package/dist/service/service.d.ts +21 -0
  400. package/dist/service/service.js +526 -0
  401. package/dist/service/warehouse-platform.d.ts +17 -0
  402. package/dist/service/warehouse-platform.js +122 -0
  403. package/dist/service/warehouse-query.d.ts +24 -0
  404. package/dist/service/warehouse-query.js +280 -0
  405. package/dist/service/webhooks.d.ts +106 -0
  406. package/dist/service/webhooks.js +233 -0
  407. package/dist/service/workflow-admission-provider.d.ts +31 -0
  408. package/dist/service/workflow-admission-provider.js +205 -0
  409. package/dist/service/workflow-argument-serialization.d.ts +2 -0
  410. package/dist/service/workflow-argument-serialization.js +14 -0
  411. package/dist/service/workflow-callback.d.ts +22 -0
  412. package/dist/service/workflow-callback.js +75 -0
  413. package/dist/service/workflow-deployment.d.ts +8 -0
  414. package/dist/service/workflow-deployment.js +131 -0
  415. package/dist/service/workflow-diagnostics.d.ts +23 -0
  416. package/dist/service/workflow-diagnostics.js +263 -0
  417. package/dist/service/workflow-errors.d.ts +26 -0
  418. package/dist/service/workflow-errors.js +181 -0
  419. package/dist/service/workflow-logging.d.ts +2 -0
  420. package/dist/service/workflow-logging.js +50 -0
  421. package/dist/service/workflow-queue-fence.d.ts +6 -0
  422. package/dist/service/workflow-queue-fence.js +66 -0
  423. package/dist/service/workflows.d.ts +200 -0
  424. package/dist/service/workflows.js +1549 -0
  425. package/dist/sign-in.d.ts +58 -0
  426. package/dist/sign-in.js +57 -0
  427. package/dist/source.d.ts +30 -0
  428. package/dist/source.js +107 -0
  429. package/dist/start-recovery-contract.d.ts +49 -0
  430. package/dist/start-recovery-contract.js +26 -0
  431. package/dist/start-recovery.d.ts +27 -0
  432. package/dist/start-recovery.js +81 -0
  433. package/dist/storage/api.d.ts +4 -0
  434. package/dist/storage/api.js +40 -0
  435. package/dist/storage/attachments.d.ts +146 -0
  436. package/dist/storage/attachments.js +733 -0
  437. package/dist/storage/authorization.d.ts +38 -0
  438. package/dist/storage/authorization.js +83 -0
  439. package/dist/storage/file-access.d.ts +17 -0
  440. package/dist/storage/file-access.js +8 -0
  441. package/dist/storage/limits.d.ts +12 -0
  442. package/dist/storage/limits.js +17 -0
  443. package/dist/storage/maintenance-auth.d.ts +20 -0
  444. package/dist/storage/maintenance-auth.js +24 -0
  445. package/dist/storage/maintenance.d.ts +11 -0
  446. package/dist/storage/maintenance.js +67 -0
  447. package/dist/storage/neon.d.ts +4 -0
  448. package/dist/storage/neon.js +318 -0
  449. package/dist/storage/operator.d.ts +11 -0
  450. package/dist/storage/operator.js +30 -0
  451. package/dist/storage/provider.d.ts +93 -0
  452. package/dist/storage/provider.js +36 -0
  453. package/dist/storage/sessions.d.ts +184 -0
  454. package/dist/storage/sessions.js +688 -0
  455. package/dist/storage/tables.d.ts +54 -0
  456. package/dist/storage/tables.js +205 -0
  457. package/dist/storage/transfers.d.ts +16 -0
  458. package/dist/storage/transfers.js +153 -0
  459. package/dist/storage/writes.d.ts +7 -0
  460. package/dist/storage/writes.js +88 -0
  461. package/dist/telegram.d.ts +35 -0
  462. package/dist/telegram.js +173 -0
  463. package/dist/user-identity.d.ts +28 -0
  464. package/dist/user-identity.js +106 -0
  465. package/dist/user.d.ts +75 -0
  466. package/dist/user.js +111 -0
  467. package/dist/warehouse-backed.d.ts +6 -0
  468. package/dist/warehouse-backed.js +495 -0
  469. package/dist/webhook-token.d.ts +11 -0
  470. package/dist/webhook-token.js +18 -0
  471. package/dist/webhooks.d.ts +40 -0
  472. package/dist/webhooks.js +55 -0
  473. package/dist/wire.d.ts +10 -0
  474. package/dist/wire.js +35 -0
  475. package/dist/workflow-runtime.d.ts +116 -0
  476. package/dist/workflow-runtime.js +109 -0
  477. package/dist/workflow-topics.d.ts +28 -0
  478. package/dist/workflow-topics.js +28 -0
  479. package/dist/workflow.d.ts +284 -0
  480. package/dist/workflow.js +259 -0
  481. package/dist/workflows.d.ts +24 -0
  482. package/dist/workflows.js +18 -0
  483. package/package.json +127 -4
@@ -0,0 +1,1307 @@
1
+ import { propertyDefinition } from "../property.js";
2
+ import { pbkdf2, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
3
+ import { promisify } from "node:util";
4
+ import { Effect, Schema } from "effect";
5
+ import { SqlClient } from "effect/unstable/sql";
6
+ import { isSqlError } from "effect/unstable/sql/SqlError";
7
+ import { entity, } from "../entity.js";
8
+ import { AGENT_IDENTITIES_TABLE, CHANNEL_USERS_TABLE, API_KEYS_TABLE, USER_ROLES_TABLE, USER_TABLE, } from "../migrate/storage.js";
9
+ import { MEMBER_ROLE, FEEDBACK_REPORTER_ROLE, toSnakeCase } from "../names.js";
10
+ import { feedbackRoles } from "../feedback-config.js";
11
+ import { buildManifest } from "../manifest.js";
12
+ import { buildOntology } from "../ontology.js";
13
+ import { EXPLICIT_GRANT_ENTITIES, permissionsOf, principalScopes, WILDCARD_ENTITY_VERBS, } from "../permissions.js";
14
+ import { EMAIL_PATTERN } from "../property.js";
15
+ import { role, roleEntries } from "../role.js";
16
+ import { beginReplicationWrite } from "../replication/transaction.js";
17
+ import { User } from "../user.js";
18
+ import { MemberNotFound, PrincipalRejected, UnknownRole } from "./errors.js";
19
+ import { channelUsersAvailable, recordUserCreation, revokeMemberSessions, } from "./member-tables.js";
20
+ import { refuseMember, requireGrantable } from "./member-writes.js";
21
+ import { isRecordId } from "./records.js";
22
+ import { makeOntologyRuntime, makePrincipalUserWriter, } from "./sql-runtime.js";
23
+ import { accountOfLegacy, CHANNEL_IDENTITIES_FROM, liveSignInIdentities, LIVE_CHANNEL_IDENTITY, LIVE_CHANNEL_RECORD, makeIdentityWriter, NEON_AUTH_PROVIDER, OIDC_PROVIDER, SIGN_IN_ORIGIN_SQL, USER_IDENTITY_TABLE, userIdentitiesAvailable, userOfAccount, } from "./user-identities.js";
24
+ import { PIPELINE_WORKER_PROVIDER } from "./workload.js";
25
+ import { WORKFLOW_USER_PROVIDER, workflowSubject } from "./workflow-users.js";
26
+ export { WORKFLOW_USER_PROVIDER, workflowSubject } from "./workflow-users.js";
27
+ export { revokeMemberSessions } from "./member-tables.js";
28
+ /** API keys are `ssk_<key id>.<secret>`. */
29
+ export const API_KEY_PREFIX = "ssk_";
30
+ /**
31
+ * PBKDF2-SHA256 iterations for a key's secret: about 5 ms per verification on Node. The secret
32
+ * already carries 256 bits of entropy, so the KDF is not what protects it; it exists so that stored
33
+ * secrets are handled like any password storage, and it must stay cheap because it runs on every
34
+ * request authenticated with an API key.
35
+ */
36
+ export const API_KEY_ITERATIONS = 10_000;
37
+ const KEY_ID_BYTES = 8;
38
+ const SECRET_BYTES = 32;
39
+ const SALT_BYTES = 16;
40
+ const HASH_BYTES = 32;
41
+ /** base64url of 8 and 32 bytes: 11 and 43 characters, none of them a dot. */
42
+ const API_KEY = /^ssk_([A-Za-z0-9_-]{11})\.([A-Za-z0-9_-]{43})$/;
43
+ const pbkdf2Async = promisify(pbkdf2);
44
+ /** A new API key: 8 random bytes of key id and 32 of secret. Show it once; it is not stored. */
45
+ export function generateApiKey() {
46
+ const keyId = randomBytes(KEY_ID_BYTES).toString("base64url");
47
+ const secret = randomBytes(SECRET_BYTES).toString("base64url");
48
+ return `${API_KEY_PREFIX}${keyId}.${secret}`;
49
+ }
50
+ /** The key id and secret of a key, or `undefined` when it is not one. */
51
+ export function parseApiKey(key) {
52
+ const match = API_KEY.exec(key);
53
+ return match === null ? undefined : { keyId: match[1], secret: match[2] };
54
+ }
55
+ /** PBKDF2-SHA256 of a secret with its salt, in hex: what `_api_keys.hash` holds. */
56
+ export async function hashApiKeySecret(secret, salt) {
57
+ const hash = await pbkdf2Async(secret, salt, API_KEY_ITERATIONS, HASH_BYTES, "sha256");
58
+ return hash.toString("hex");
59
+ }
60
+ /** Compared against when no row matches, so that every rejection costs one KDF. */
61
+ const UNKNOWN_KEY = { salt: randomBytes(SALT_BYTES).toString("hex"), hash: "0".repeat(64) };
62
+ function sameHash(left, right) {
63
+ const leftBytes = Buffer.from(left, "hex");
64
+ const rightBytes = Buffer.from(right, "hex");
65
+ return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes);
66
+ }
67
+ /**
68
+ * Every request authenticates, so recording each one would write a history row per request. A
69
+ * returning member's sign-in is recorded once the last one is older than a Neon Auth token's
70
+ * lifetime: about once per token, which is how often an active member authenticates afresh.
71
+ */
72
+ export const SIGN_IN_RECORD_INTERVAL_MS = 15 * 60 * 1000;
73
+ /** Whether a returning member's sign-in is recorded: they have none yet, or none this interval. */
74
+ function signInDue(lastSignInAt, now) {
75
+ if (typeof lastSignInAt !== "string") {
76
+ return true;
77
+ }
78
+ const last = Date.parse(lastSignInAt);
79
+ return Number.isNaN(last) || now.getTime() - last >= SIGN_IN_RECORD_INTERVAL_MS;
80
+ }
81
+ /** User rows as principal resolution reads them. */
82
+ const UserRow = Schema.Struct({
83
+ id: Schema.String,
84
+ kind: Schema.Literals(["member", "service"]),
85
+ active: Schema.Boolean,
86
+ });
87
+ const decodeUser = Schema.decodeUnknownSync(UserRow);
88
+ /** Writes the platform makes outside any principal's action: history names them, without a principal. */
89
+ function platformRuntime(ontology, sql, action, actionId = randomUUID()) {
90
+ return makeOntologyRuntime(ontology, sql, { action, actionId, principalId: null });
91
+ }
92
+ /** The history actions of member role changes, as the generated `user` actions name them. */
93
+ export const MEMBER_HISTORY_ACTIONS = {
94
+ invite: "user.invite",
95
+ grantRoles: "user.grant_roles",
96
+ revokeRoles: "user.revoke_roles",
97
+ deactivate: "user.deactivate",
98
+ reactivate: "user.reactivate",
99
+ };
100
+ export function rolesOf(ontology, sql, user) {
101
+ return Effect.gen(function* () {
102
+ const rows = yield* sql.unsafe(`SELECT "role" FROM "${USER_ROLES_TABLE}" WHERE "user_id" = $1::uuid ORDER BY "role"`, [user.id]);
103
+ // Ignore forged or historical stored assignments: reporter authority is never persisted.
104
+ const assigned = rows
105
+ .map((row) => String(row.role))
106
+ .filter((role) => toSnakeCase(role) !== FEEDBACK_REPORTER_ROLE);
107
+ // Every active member holds `member`; a service holds exactly the roles it was given.
108
+ const roles = user.kind === "member"
109
+ ? [
110
+ MEMBER_ROLE,
111
+ ...assigned.filter((role) => role !== MEMBER_ROLE),
112
+ ...(ontology.roles.some((role) => role.name === FEEDBACK_REPORTER_ROLE)
113
+ ? [FEEDBACK_REPORTER_ROLE]
114
+ : []),
115
+ ]
116
+ : assigned;
117
+ // A role the deployed ontology no longer declares grants nothing.
118
+ return {
119
+ id: user.id,
120
+ kind: user.kind,
121
+ roles,
122
+ permissions: permissionsOf(ontology.roles, roles),
123
+ ...principalScopes(ontology.roles, roles),
124
+ };
125
+ });
126
+ }
127
+ function activePrincipal(ontology, sql, user) {
128
+ return Effect.gen(function* () {
129
+ if (!user.active || (user.kind === "member" && (yield* isMemberBlocked(sql, user.id)))) {
130
+ return yield* new PrincipalRejected({
131
+ reason: "inactive_user",
132
+ message: "The user is inactive.",
133
+ });
134
+ }
135
+ return yield* rolesOf(ontology, sql, user);
136
+ });
137
+ }
138
+ function userRow(record) {
139
+ return decodeUser({ id: record.id, kind: record.kind, active: record.active });
140
+ }
141
+ /** Resolve a workspace-owned identity/session by user id, with current activity and roles. */
142
+ export function resolvePrincipalById(ontology, sql, id) {
143
+ return Effect.gen(function* () {
144
+ const runtime = makeOntologyRuntime(ontology, sql);
145
+ const [record] = yield* runtime.find({ entity: "user", options: { where: { id }, limit: 1 } });
146
+ if (!record || record.kind !== "member") {
147
+ return yield* new PrincipalRejected({
148
+ reason: "identity_conflict",
149
+ message: "Unknown workspace member.",
150
+ });
151
+ }
152
+ return yield* activePrincipal(ontology, sql, userRow(record));
153
+ });
154
+ }
155
+ /**
156
+ * Finds the member's user without locking. Links or creates it transactionally when needed. A
157
+ * user linked to this identity is found; else a member invited with the same email is linked; else
158
+ * the ontology's sign-in policy decides: `invited` turns the person away, `open` creates a member.
159
+ */
160
+ function signIn(ontology, sql, claims) {
161
+ const runtime = makeOntologyRuntime(ontology, sql);
162
+ const writerActionId = randomUUID();
163
+ const writer = makePrincipalUserWriter(ontology, sql, {
164
+ action: "user.sign_in",
165
+ actionId: writerActionId,
166
+ });
167
+ const claimedEmail = claims.email?.trim().toLowerCase();
168
+ const email = claimedEmail !== undefined && EMAIL_PATTERN.test(claimedEmail) ? claimedEmail : undefined;
169
+ // The account the token names, as `user_identity` holds it: `oidc:<issuer>` is an oidc account.
170
+ const account = accountOfLegacy(claims.identityProvider, claims.identitySubject);
171
+ const sameAccount = (identity) => identity.provider === account.provider &&
172
+ identity.issuer === account.issuer &&
173
+ identity.subject === account.subject;
174
+ // The member's `user_identity` record finds them; their user row is what is locked. Without one,
175
+ // the user columns still find a member an earlier SDK signed in during a cutover, until the next
176
+ // migration copies the account, and a service, which is found to be refused.
177
+ const findLinked = (forUpdate = false) => Effect.gen(function* () {
178
+ const userId = yield* userOfAccount(sql, account);
179
+ return yield* runtime.find({
180
+ entity: "user",
181
+ options: {
182
+ where: userId === undefined
183
+ ? {
184
+ identity_provider: claims.identityProvider,
185
+ identity_subject: claims.identitySubject,
186
+ }
187
+ : { id: userId },
188
+ limit: 1,
189
+ forUpdate,
190
+ },
191
+ });
192
+ });
193
+ // The user columns stay written for SDKs before `user_identity`; the record is written beside.
194
+ const recordIdentity = (userId, origin) => makeIdentityWriter(ontology, sql, {
195
+ action: `${USER_IDENTITY_TABLE}.sign_in`,
196
+ actionId: writerActionId,
197
+ }).create({ ...account, userId, origin });
198
+ const member = (record) => record.kind === "member"
199
+ ? Effect.succeed(userRow(record))
200
+ : Effect.fail(new PrincipalRejected({
201
+ reason: "identity_conflict",
202
+ message: "This identity belongs to a service, not a member.",
203
+ }));
204
+ return Effect.gen(function* () {
205
+ const [known] = yield* findLinked();
206
+ if (known !== undefined) {
207
+ const user = yield* member(known);
208
+ // Ordinary authentication is read-only. Optional bookkeeping must never queue behind a
209
+ // business transaction holding a User key or row lock; a later request can record it.
210
+ if (!user.active || !writer.recordsSignIn || !signInDue(known.last_sign_in_at, new Date())) {
211
+ return user;
212
+ }
213
+ return yield* sql.withTransaction(Effect.gen(function* () {
214
+ const [locked] = yield* sql.unsafe(`SELECT "id" FROM "${USER_TABLE}" WHERE "id" = $1::uuid FOR UPDATE SKIP LOCKED`, [user.id]);
215
+ if (locked === undefined) {
216
+ return user;
217
+ }
218
+ const current = yield* runtime.get({ entity: "user", id: user.id });
219
+ const principal = yield* member(current);
220
+ const now = new Date();
221
+ if (principal.active && signInDue(current.last_sign_in_at, now)) {
222
+ yield* writer.update(user.id, { last_sign_in_at: now.toISOString() });
223
+ }
224
+ return principal;
225
+ }));
226
+ }
227
+ // CLI file commands use trusted SegmentStream app tokens while the browser uses Neon Auth.
228
+ // Both issuers verify the email. Resolve the existing member without replacing their Neon
229
+ // subject: browser sessions, pairings and current role assignments must keep the same user.
230
+ // A different subject of the same provider still conflicts, as do service users and tokens
231
+ // @legacy app.legacy-segmentstream-sign-in
232
+ // without a verified email. Only the platform-to-Neon compatibility path uses this lookup.
233
+ if (claims.identityProvider === "segmentstream" && email !== undefined) {
234
+ const [neon] = yield* sql.unsafe(`SELECT "u"."id"::text AS "id", "u"."kind"::text AS "kind", "u"."active" AS "active" ` +
235
+ `FROM "${USER_TABLE}" AS "u" WHERE "u"."email" = $1 AND "u"."kind" = 'member' ` +
236
+ `AND EXISTS (SELECT 1 FROM "${USER_IDENTITY_TABLE}" AS "i" WHERE "i"."user_id" = "u"."id" ` +
237
+ `AND "i"."provider" = $2 AND "i"."origin"::text IN ${SIGN_IN_ORIGIN_SQL} ` +
238
+ `AND "i"."revoked_at" IS NULL) LIMIT 1`, [email, NEON_AUTH_PROVIDER]);
239
+ if (neon !== undefined) {
240
+ return decodeUser(neon);
241
+ }
242
+ }
243
+ return yield* sql.withTransaction(Effect.gen(function* () {
244
+ if (claims.identityProvider === "neon-auth") {
245
+ yield* sql.unsafe(`SELECT pg_advisory_xact_lock_shared(hashtextextended('segmentstream.ontology.migration', 0))`);
246
+ const [catalog] = yield* sql.unsafe(`SELECT manifest->'sign_in'->'browser'->'methods' AS methods FROM "_ontology" WHERE id`);
247
+ if (Array.isArray(catalog?.methods) && !catalog.methods.includes("email")) {
248
+ return yield* new PrincipalRejected({
249
+ reason: "identity_conflict",
250
+ message: "browser_method_disabled",
251
+ });
252
+ }
253
+ }
254
+ const now = new Date();
255
+ const signedIn = writer.recordsSignIn ? { last_sign_in_at: now.toISOString() } : {};
256
+ const [linked] = yield* findLinked(true);
257
+ if (linked !== undefined) {
258
+ return yield* member(linked);
259
+ }
260
+ const [invited] = email === undefined
261
+ ? []
262
+ : yield* runtime.find({
263
+ entity: "user",
264
+ options: { where: { email }, limit: 1, forUpdate: true },
265
+ });
266
+ if (invited !== undefined) {
267
+ // A concurrent first sign-in may have linked this invitation while this transaction
268
+ // waited for its row. Accept the exact identity; another identity still conflicts.
269
+ const held = invited.kind === "member" ? yield* liveSignInIdentities(sql, String(invited.id)) : [];
270
+ // An earlier SDK serving the workspace during a cutover writes only the user's columns.
271
+ // Its sign-in still holds the invitation until the next migration copies it.
272
+ const legacy = typeof invited.identity_subject === "string" && invited.identity_subject !== ""
273
+ ? {
274
+ provider: String(invited.identity_provider ?? ""),
275
+ subject: invited.identity_subject,
276
+ }
277
+ : undefined;
278
+ const legacyIsThisAccount = legacy !== undefined &&
279
+ legacy.provider === claims.identityProvider &&
280
+ legacy.subject === claims.identitySubject;
281
+ if (held.some(sameAccount)) {
282
+ return yield* member(invited);
283
+ }
284
+ if (invited.kind === "member" && held.length === 0 && legacyIsThisAccount) {
285
+ yield* recordIdentity(String(invited.id), "invite");
286
+ return yield* member(invited);
287
+ }
288
+ if (invited.kind !== "member" ||
289
+ held.length > 0 ||
290
+ legacy !== undefined ||
291
+ (account.provider === OIDC_PROVIDER && invited.active !== true)) {
292
+ return yield* new PrincipalRejected({
293
+ reason: "identity_conflict",
294
+ message: "Another user already has this email.",
295
+ });
296
+ }
297
+ yield* recordUserCreation(sql, String(invited.id), "invite");
298
+ const linkedUser = yield* writer.update(invited.id, {
299
+ identity_provider: claims.identityProvider,
300
+ identity_subject: claims.identitySubject,
301
+ ...(invited.active === true ? signedIn : {}),
302
+ });
303
+ yield* recordIdentity(String(invited.id), "invite");
304
+ return userRow(linkedUser);
305
+ }
306
+ if (ontology.signIn.policy !== "open") {
307
+ return yield* new PrincipalRejected({
308
+ reason: "not_invited",
309
+ message: "No member was invited with this identity or email.",
310
+ });
311
+ }
312
+ const created = yield* writer.create({
313
+ name: claims.name?.trim() || email || claims.identitySubject,
314
+ email: email ?? null,
315
+ kind: "member",
316
+ identity_provider: claims.identityProvider,
317
+ identity_subject: claims.identitySubject,
318
+ ...signedIn,
319
+ });
320
+ yield* recordUserCreation(sql, String(created.id), "email");
321
+ yield* recordIdentity(String(created.id), "sign_in");
322
+ return userRow(created);
323
+ }));
324
+ }).pipe(Effect.catchTag("NotFound", (error) => Effect.die(error)));
325
+ }
326
+ /**
327
+ * The principal of a member signed in with a verified token. On first sign-in the member's `user`
328
+ * is linked by email when someone invited them; a person nobody invited is rejected as
329
+ * `not_invited` under the `invited` policy and becomes a member holding only `member` under
330
+ * `open`. Every sign-in that lets the member in sets `lastSignInAt`, at most once per
331
+ * {@link SIGN_IN_RECORD_INTERVAL_MS}, so history keeps it. If the User row is busy, recording is
332
+ * deferred to a later request instead of delaying authentication. Inactive users are rejected.
333
+ */
334
+ export function resolvePrincipalByIdentity(ontology, claims) {
335
+ return Effect.gen(function* () {
336
+ const sql = yield* SqlClient.SqlClient;
337
+ const attempt = signIn(ontology, sql, claims);
338
+ // Two first sign-ins of one member race to create the user; the loser finds it on retry.
339
+ const user = yield* attempt.pipe(Effect.catchTag("Conflict", () => attempt), Effect.catchTag("Conflict", () => Effect.fail(new PrincipalRejected({
340
+ reason: "identity_conflict",
341
+ message: "The sign-in conflicts with another user.",
342
+ }))));
343
+ return yield* activePrincipal(ontology, sql, user);
344
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
345
+ }
346
+ /** Resolve the workspace's stable keyless pipeline service, without customer roles or sign-in. */
347
+ export function resolvePipelineWorker(ontology, execution) {
348
+ return Effect.gen(function* () {
349
+ const sql = yield* SqlClient.SqlClient;
350
+ const provider = PIPELINE_WORKER_PROVIDER;
351
+ const subject = `pipeline-workers:${execution.workspaceId}`;
352
+ const runtime = platformRuntime(ontology, sql, "user.resolve_pipeline_worker");
353
+ const principal = (user) => user.kind !== "service" || user.active !== true
354
+ ? Effect.fail(new PrincipalRejected({
355
+ reason: "inactive_user",
356
+ message: "The pipeline worker principal is inactive or has an invalid kind.",
357
+ }))
358
+ : Effect.succeed({
359
+ id: user.id,
360
+ kind: "service",
361
+ roles: [],
362
+ permissions: ["*:*"],
363
+ execution,
364
+ });
365
+ const find = (forUpdate) => runtime.find({
366
+ entity: "user",
367
+ options: {
368
+ where: { identity_provider: provider, identity_subject: subject },
369
+ limit: 1,
370
+ forUpdate,
371
+ },
372
+ });
373
+ const [known] = yield* find(false);
374
+ // Returning requests only read the principal; writes revalidate it in their transaction.
375
+ if (known !== undefined) {
376
+ return yield* principal(known);
377
+ }
378
+ return yield* sql
379
+ .withTransaction(Effect.gen(function* () {
380
+ // The replication lock precedes every business lock, including principal initialization.
381
+ yield* beginReplicationWrite(sql);
382
+ yield* sql.unsafe("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [
383
+ provider + ":" + subject,
384
+ ]);
385
+ const [existing] = yield* find(true);
386
+ const user = existing ??
387
+ (yield* runtime.create({
388
+ entity: "user",
389
+ values: {
390
+ name: "Pipeline workers",
391
+ kind: "service",
392
+ identity_provider: provider,
393
+ identity_subject: subject,
394
+ },
395
+ }));
396
+ return yield* principal(user);
397
+ }))
398
+ .pipe(Effect.catchTag("Conflict", (error) => Effect.die(error)), Effect.catchTag(["NotFound", "ScopeViolated"], (error) => Effect.die(error)));
399
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
400
+ }
401
+ /** A keyless hook actor with only the workspace's opted-in role; deactivated actors stay inactive. */
402
+ export function resolveHookService(ontology, heldRole) {
403
+ return Effect.gen(function* () {
404
+ const sql = yield* SqlClient.SqlClient;
405
+ const configured = ontology.config.hooks?.role;
406
+ if (configured === undefined ||
407
+ heldRole === FEEDBACK_REPORTER_ROLE ||
408
+ toSnakeCase(configured) !== heldRole ||
409
+ !ontology.roles.some((candidate) => candidate.name === heldRole)) {
410
+ return yield* new PrincipalRejected({
411
+ reason: "identity_conflict",
412
+ message: "Hook service execution is not configured for this role.",
413
+ });
414
+ }
415
+ const provider = "segmentstream-ontology-hook";
416
+ const subject = heldRole;
417
+ const runtime = platformRuntime(ontology, sql, "user.resolve_hook_service");
418
+ const principal = (user) => user.kind !== "service" || user.active !== true
419
+ ? Effect.fail(new PrincipalRejected({
420
+ reason: "inactive_user",
421
+ message: "The hook service principal is inactive or has an invalid kind.",
422
+ }))
423
+ : rolesOf(ontology, sql, userRow(user)).pipe(Effect.map((resolved) => {
424
+ const roles = resolved.roles.includes(heldRole) ? [heldRole] : [];
425
+ return {
426
+ id: resolved.id,
427
+ kind: resolved.kind,
428
+ roles,
429
+ permissions: permissionsOf(ontology.roles, roles),
430
+ ...principalScopes(ontology.roles, roles),
431
+ };
432
+ }));
433
+ const find = (forUpdate) => runtime.find({
434
+ entity: "user",
435
+ options: {
436
+ where: { identity_provider: provider, identity_subject: subject },
437
+ limit: 1,
438
+ forUpdate,
439
+ },
440
+ });
441
+ const [known] = yield* find(false);
442
+ if (known !== undefined) {
443
+ return yield* principal(known);
444
+ }
445
+ return yield* sql
446
+ .withTransaction(Effect.gen(function* () {
447
+ yield* beginReplicationWrite(sql);
448
+ yield* sql.unsafe("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [
449
+ provider + ":" + subject,
450
+ ]);
451
+ const [existing] = yield* find(true);
452
+ const user = existing ??
453
+ (yield* runtime.create({
454
+ entity: "user",
455
+ values: {
456
+ name: configured,
457
+ kind: "service",
458
+ identity_provider: provider,
459
+ identity_subject: subject,
460
+ },
461
+ }));
462
+ if (existing === undefined) {
463
+ yield* sql.unsafe(`INSERT INTO "${USER_ROLES_TABLE}" ("user_id", "role") VALUES ($1::uuid, $2::text)`, [user.id, heldRole]);
464
+ }
465
+ return yield* principal(user);
466
+ }))
467
+ .pipe(Effect.catchTag("Conflict", (error) => Effect.die(error)), Effect.catchTag(["NotFound", "ScopeViolated"], (error) => Effect.die(error)));
468
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
469
+ }
470
+ /**
471
+ * A workflow user's permissions: what `member` and the workflow's role grant, as `check` verified.
472
+ * A `*:*` of member's is spelled out per entity, so that it covers what a named grant covers and
473
+ * never the pipeline worker's workload wildcard; `check` refuses a workflow role holding `*:*`. An
474
+ * entity a wildcard covers only some verbs of, the user, is spelled out with those verbs.
475
+ */
476
+ export function workflowPermissions(ontology, role) {
477
+ if (toSnakeCase(role) === FEEDBACK_REPORTER_ROLE) {
478
+ return [];
479
+ }
480
+ return permissionsOf(ontology.roles, [MEMBER_ROLE, role]).flatMap((permission) => permission === "*:*"
481
+ ? ontology.entities
482
+ .filter((candidate) => !EXPLICIT_GRANT_ENTITIES.has(candidate.name))
483
+ .flatMap((candidate) => {
484
+ const verbs = WILDCARD_ENTITY_VERBS.get(candidate.name);
485
+ return verbs === undefined
486
+ ? [`${candidate.name}:*`]
487
+ : [...verbs].map((verb) => `${candidate.name}:${verb}`);
488
+ })
489
+ : [permission]);
490
+ }
491
+ /** The roles assigned to a user, sorted. */
492
+ function assignedRoles(sql, userId) {
493
+ return sql
494
+ .unsafe(`SELECT "role" FROM "${USER_ROLES_TABLE}" WHERE "user_id" = $1::uuid ORDER BY "role"`, [userId])
495
+ .pipe(Effect.map((rows) => rows.map((row) => row.role)));
496
+ }
497
+ /** Sets the user's roles to exactly the workflow's role when they differ, with the role writer. */
498
+ function syncWorkflowRole(ontology, sql, userId, role) {
499
+ return Effect.gen(function* () {
500
+ const held = yield* assignedRoles(sql, userId);
501
+ if (held.length !== 1 || held[0] !== role) {
502
+ // `check` verified the role is declared; an undeclared one here is a defect.
503
+ yield* writeUserRoles(ontology, sql, { userId, roles: [role] }).pipe(Effect.orDie);
504
+ }
505
+ });
506
+ }
507
+ /**
508
+ * The workflow's user, created if missing, with exactly the workflow's role, inside the caller's
509
+ * transaction and under the lock every creator of this user takes. It does not change `active`.
510
+ */
511
+ function ensureWorkflowUser(ontology, sql, runtime, input) {
512
+ return Effect.gen(function* () {
513
+ yield* sql.unsafe("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [
514
+ WORKFLOW_USER_PROVIDER + ":" + input.subject,
515
+ ]);
516
+ const [existing] = yield* runtime.find({
517
+ entity: "user",
518
+ options: {
519
+ where: { identity_provider: WORKFLOW_USER_PROVIDER, identity_subject: input.subject },
520
+ limit: 1,
521
+ forUpdate: true,
522
+ },
523
+ });
524
+ if (existing !== undefined && input.mode === "resolve") {
525
+ const held = yield* assignedRoles(sql, existing.id);
526
+ if (held.length !== 1 || held[0] !== input.workflow.role) {
527
+ return yield* new PrincipalRejected({
528
+ reason: "role_mismatch",
529
+ message: "The workflow user's roles are not the workflow's declared role; the deploy that " +
530
+ "recorded this ontology reconciles them.",
531
+ });
532
+ }
533
+ return existing;
534
+ }
535
+ const user = existing ??
536
+ (yield* runtime
537
+ .create({
538
+ entity: "user",
539
+ values: {
540
+ name: input.workflow.name,
541
+ kind: "service",
542
+ identity_provider: WORKFLOW_USER_PROVIDER,
543
+ identity_subject: input.subject,
544
+ },
545
+ })
546
+ .pipe(Effect.orDie));
547
+ yield* syncWorkflowRole(ontology, sql, user.id, input.workflow.role);
548
+ return user;
549
+ });
550
+ }
551
+ /**
552
+ * Resolve the keyless service user a workflow's runs act as: one per declared workflow, holding
553
+ * exactly the workflow's role, named by `workflows:<workspace id>:<workflow name>`. Deploy creates
554
+ * it; a run before that creates it here. It never rewrites roles: a user whose roles are not
555
+ * exactly the declared role is rejected (`role_mismatch`) until a deploy reconciles it. A workflow
556
+ * the serving ontology does not declare is rejected, as is an inactive user. Its permissions are
557
+ * what `member` and its role grant, and every action checks them as for any caller.
558
+ */
559
+ export function resolveWorkflowUser(ontology, input) {
560
+ return Effect.gen(function* () {
561
+ const sql = yield* SqlClient.SqlClient;
562
+ const declared = ontology.workflows.find((candidate) => candidate.name === input.workflow);
563
+ if (declared === undefined || toSnakeCase(declared.role) === FEEDBACK_REPORTER_ROLE) {
564
+ return yield* new PrincipalRejected({
565
+ reason: "unknown_workflow",
566
+ message: "The serving ontology declares no such workflow.",
567
+ });
568
+ }
569
+ const subject = workflowSubject(input.workspaceId, declared.name);
570
+ const runtime = platformRuntime(ontology, sql, "user.resolve_workflow_user");
571
+ const [known] = yield* runtime.find({
572
+ entity: "user",
573
+ options: {
574
+ where: { identity_provider: WORKFLOW_USER_PROVIDER, identity_subject: subject },
575
+ limit: 1,
576
+ },
577
+ });
578
+ const held = known === undefined ? [] : yield* assignedRoles(sql, known.id);
579
+ // The deployed database is the authority on roles: deploy sets them, a run never rewrites them.
580
+ if (known !== undefined && (held.length !== 1 || held[0] !== declared.role)) {
581
+ return yield* new PrincipalRejected({
582
+ reason: "role_mismatch",
583
+ message: "The workflow user's roles are not the workflow's declared role; the deploy that " +
584
+ "recorded this ontology reconciles them.",
585
+ });
586
+ }
587
+ // A run of a deployed workflow only reads; a run before its deploy creates the user.
588
+ const user = known !== undefined
589
+ ? known
590
+ : yield* sql.withTransaction(Effect.gen(function* () {
591
+ // The replication lock precedes every business lock, including principal creation.
592
+ yield* beginReplicationWrite(sql);
593
+ return yield* ensureWorkflowUser(ontology, sql, runtime, {
594
+ workflow: declared,
595
+ subject,
596
+ mode: "resolve",
597
+ });
598
+ }));
599
+ if (user.kind !== "service" || user.active !== true) {
600
+ return yield* new PrincipalRejected({
601
+ reason: "inactive_user",
602
+ message: "The workflow user is inactive or has an invalid kind.",
603
+ });
604
+ }
605
+ return {
606
+ id: user.id,
607
+ kind: "service",
608
+ roles: [declared.role],
609
+ permissions: workflowPermissions(ontology, declared.role),
610
+ ...principalScopes(ontology.roles, [MEMBER_ROLE, declared.role]),
611
+ workflow: { name: declared.name, subject },
612
+ };
613
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
614
+ }
615
+ /**
616
+ * Brings the workflow users of the workspace in line with the ontology's workflows, as deploy does
617
+ * in the migration's transaction: each declared workflow's user is created if missing, reactivated
618
+ * and given exactly the workflow's role; a user whose workflow is no longer declared is
619
+ * deactivated, so its runs are refused and history keeps its name. Repeating it changes nothing.
620
+ * Database failures, including those of the history writes, fail with their `SqlError`.
621
+ */
622
+ export function reconcileWorkflowUsers(ontology, input) {
623
+ return Effect.gen(function* () {
624
+ const sql = yield* SqlClient.SqlClient;
625
+ const [table] = yield* sql.unsafe(`SELECT to_regclass($1) IS NOT NULL AS "present"`, [`"${USER_TABLE}"`]);
626
+ if (table?.present !== true) {
627
+ return;
628
+ }
629
+ const rows = yield* sql.unsafe(`SELECT "id"::text AS "id", "identity_subject" AS "subject" FROM "${USER_TABLE}" ` +
630
+ `WHERE "identity_provider" = $1::text ORDER BY "id"`, [WORKFLOW_USER_PROVIDER]);
631
+ if (rows.length === 0 && ontology.workflows.length === 0) {
632
+ return;
633
+ }
634
+ const declared = new Map(ontology.workflows.map((candidate) => [
635
+ workflowSubject(input.workspaceId, candidate.name),
636
+ candidate,
637
+ ]));
638
+ const setActive = (id, active) => Effect.gen(function* () {
639
+ const runtime = platformRuntime(ontology, sql, active ? "user.reactivate_workflow_user" : "user.deactivate_workflow_user");
640
+ const user = yield* runtime.get({ entity: "user", id, forUpdate: true });
641
+ if (user.active !== active) {
642
+ yield* runtime.update({ entity: "user", id, values: { active } });
643
+ }
644
+ });
645
+ yield* sql.withTransaction(Effect.gen(function* () {
646
+ yield* beginReplicationWrite(sql);
647
+ // Users of other workspaces' subjects, or of removed workflows, may not act here.
648
+ for (const row of rows) {
649
+ if (row.subject === null || !declared.has(row.subject)) {
650
+ yield* setActive(row.id, false);
651
+ }
652
+ }
653
+ for (const [subject, workflow] of declared) {
654
+ const user = yield* ensureWorkflowUser(ontology, sql, platformRuntime(ontology, sql, "user.reconcile_workflow_user"), { workflow, subject, mode: "reconcile" }).pipe(Effect.catchTag("PrincipalRejected", (error) => Effect.die(error)));
655
+ yield* setActive(user.id, true);
656
+ }
657
+ }));
658
+ }).pipe(Effect.catchTag(["Conflict", "NotFound", "ScopeViolated"], (error) => Effect.die(error)),
659
+ // The runtime treats database failures as defects; deploy classifies them as failures.
660
+ Effect.catchDefect((defect) => (isSqlError(defect) ? Effect.fail(defect) : Effect.die(defect))));
661
+ }
662
+ /**
663
+ * The principal of an API key, with the permissions its roles have in the ontology. Unknown and
664
+ * revoked keys and inactive users are rejected.
665
+ */
666
+ export function resolvePrincipalByApiKey(ontology, key) {
667
+ return Effect.gen(function* () {
668
+ const sql = yield* SqlClient.SqlClient;
669
+ const unknownKey = (diagnostic) => new PrincipalRejected({
670
+ reason: "unknown_api_key",
671
+ message: "The API key is not valid.",
672
+ diagnostic,
673
+ });
674
+ const parts = parseApiKey(key);
675
+ if (parts === undefined) {
676
+ return yield* unknownKey("malformed_api_key");
677
+ }
678
+ const [row] = yield* sql.unsafe(`SELECT "u"."id"::text AS "id", "u"."kind"::text AS "kind", "u"."active" AS "active", ` +
679
+ `"k"."salt" AS "salt", "k"."hash" AS "hash", "k"."revoked_at" IS NOT NULL AS "revoked" ` +
680
+ `FROM "${API_KEYS_TABLE}" AS "k" JOIN "${USER_TABLE}" AS "u" ON "u"."id" = "k"."user_id" ` +
681
+ `WHERE "k"."key_id" = $1::text`, [parts.keyId]);
682
+ // An unknown key id costs one KDF too, so that timing does not tell which key ids exist.
683
+ const stored = typeof row?.salt === "string" && typeof row.hash === "string"
684
+ ? { salt: row.salt, hash: row.hash }
685
+ : UNKNOWN_KEY;
686
+ const hash = yield* Effect.promise(() => hashApiKeySecret(parts.secret, stored.salt));
687
+ if (row === undefined) {
688
+ return yield* unknownKey("missing_api_key");
689
+ }
690
+ if (stored === UNKNOWN_KEY) {
691
+ return yield* unknownKey("invalid_api_key_record");
692
+ }
693
+ if (!sameHash(hash, stored.hash)) {
694
+ return yield* unknownKey("api_key_secret_mismatch");
695
+ }
696
+ if (row.revoked === true) {
697
+ return yield* new PrincipalRejected({
698
+ reason: "revoked_api_key",
699
+ message: "The API key was revoked.",
700
+ });
701
+ }
702
+ return yield* activePrincipal(ontology, sql, decodeUser({ id: row.id, kind: row.kind, active: row.active }));
703
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
704
+ }
705
+ /**
706
+ * A member by user id, with the roles they hold now: whom an OAuth grant acts for. `undefined` for
707
+ * an unknown user, a service and an inactive member.
708
+ */
709
+ export function resolveMemberById(ontology, userId) {
710
+ return Effect.gen(function* () {
711
+ const sql = yield* SqlClient.SqlClient;
712
+ const [row] = yield* sql.unsafe(`SELECT "id"::text AS "id", "kind"::text AS "kind", "active" AS "active" ` +
713
+ `FROM "${USER_TABLE}" WHERE "id" = $1::uuid`, [userId]);
714
+ if (row === undefined ||
715
+ row.kind !== "member" ||
716
+ row.active !== true ||
717
+ (yield* isMemberBlocked(sql, userId))) {
718
+ return undefined;
719
+ }
720
+ return yield* rolesOf(ontology, sql, decodeUser(row));
721
+ }).pipe(Effect.orDie);
722
+ }
723
+ /** Issues an API key for a user. */
724
+ export function createApiKey(input) {
725
+ return Effect.gen(function* () {
726
+ const sql = yield* SqlClient.SqlClient;
727
+ const id = randomUUID();
728
+ const key = generateApiKey();
729
+ const { keyId, secret } = parseApiKey(key);
730
+ const salt = randomBytes(SALT_BYTES).toString("hex");
731
+ const hash = yield* Effect.promise(() => hashApiKeySecret(secret, salt));
732
+ yield* sql.unsafe(`INSERT INTO "${API_KEYS_TABLE}" ` +
733
+ `("id", "user_id", "key_id", "salt", "hash", "label", "created_at") ` +
734
+ "VALUES ($1::uuid, $2::uuid, $3::text, $4::text, $5::text, $6::text, now())", [id, input.userId, keyId, salt, hash, input.label]);
735
+ return { id, key };
736
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
737
+ }
738
+ /** Revokes an API key. Revoking it again changes nothing. */
739
+ export function revokeApiKey(input) {
740
+ return Effect.gen(function* () {
741
+ const sql = yield* SqlClient.SqlClient;
742
+ if (!isRecordId(input.id)) {
743
+ return;
744
+ }
745
+ yield* sql.unsafe(`UPDATE "${API_KEYS_TABLE}" SET "revoked_at" = now() WHERE "id" = $1::uuid AND "revoked_at" IS NULL`, [input.id]);
746
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
747
+ }
748
+ /** Replaces a user's roles inside the caller's transaction, failing on a role the ontology lacks. */
749
+ function writeUserRoles(ontology, sql, input) {
750
+ return Effect.gen(function* () {
751
+ const known = new Set(ontology.roles.map((candidate) => candidate.name));
752
+ const unknown = input.roles.find((role) => toSnakeCase(role) === FEEDBACK_REPORTER_ROLE || !known.has(role));
753
+ if (unknown !== undefined) {
754
+ return yield* new UnknownRole({
755
+ role: unknown,
756
+ message: `${unknown} is not a role of this ontology.`,
757
+ });
758
+ }
759
+ const roles = [...new Set(input.roles)];
760
+ yield* sql.unsafe(`DELETE FROM "${USER_ROLES_TABLE}" WHERE "user_id" = $1::uuid`, [
761
+ input.userId,
762
+ ]);
763
+ for (const role of roles) {
764
+ yield* sql.unsafe(`INSERT INTO "${USER_ROLES_TABLE}" ("user_id", "role") VALUES ($1::uuid, $2::text)`, [input.userId, role]);
765
+ }
766
+ });
767
+ }
768
+ /** Replaces the roles a user was given. Members hold `member` whatever is assigned. */
769
+ export function setUserRoles(ontology, input) {
770
+ return Effect.gen(function* () {
771
+ const sql = yield* SqlClient.SqlClient;
772
+ yield* sql.withTransaction(Effect.gen(function* () {
773
+ yield* beginReplicationWrite(sql);
774
+ yield* writeUserRoles(ontology, sql, input);
775
+ }));
776
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
777
+ }
778
+ /**
779
+ * Creates a service user with roles and a first API key, as the app or the CLI does for the
780
+ * website's intake key.
781
+ */
782
+ export function createServiceUser(ontology, input) {
783
+ return Effect.gen(function* () {
784
+ const sql = yield* SqlClient.SqlClient;
785
+ const runtime = platformRuntime(ontology, sql, "user.create_service");
786
+ return yield* sql.withTransaction(Effect.gen(function* () {
787
+ yield* beginReplicationWrite(sql);
788
+ const user = yield* runtime
789
+ .create({ entity: "user", values: { name: input.name, kind: "service" } })
790
+ .pipe(Effect.orDie);
791
+ yield* setUserRoles(ontology, { userId: user.id, roles: input.roles });
792
+ const apiKey = yield* createApiKey({ userId: user.id, label: input.keyLabel });
793
+ return { user, apiKey };
794
+ }));
795
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
796
+ }
797
+ /**
798
+ * The built-in `User` as the deployed table has it: only the properties the manifest lists, and
799
+ * @legacy ontology.pre-upgrade-database-tolerance
800
+ * `roles`, which has no column. A manifest an earlier SDK built lacks the properties added since,
801
+ * such as `lastSignInAt`, and its table has no column for them until the workspace is deployed with
802
+ * this SDK.
803
+ */
804
+ function deployedUser(manifest) {
805
+ const deployed = manifest.entities?.find((candidate) => candidate.builtIn);
806
+ if (deployed === undefined) {
807
+ return User;
808
+ }
809
+ const names = new Set(deployed.properties.map((property) => property.name));
810
+ // A list has no column: the deployed table serves it whatever the manifest lists.
811
+ const kept = Object.entries(User.definition.properties).filter(([name, property]) => propertyDefinition(property)?.type === "list" ||
812
+ names.has(toSnakeCase(name)) ||
813
+ (propertyDefinition(property)?.renamedFrom !== undefined &&
814
+ names.has(propertyDefinition(property).renamedFrom)));
815
+ if (kept.length === Object.keys(User.definition.properties).length) {
816
+ return User;
817
+ }
818
+ const definition = {
819
+ ...User.definition,
820
+ properties: Object.fromEntries(kept),
821
+ };
822
+ return entity(User.name, definition);
823
+ }
824
+ /**
825
+ * An ontology with only the built-in `User` and the manifest's roles, with their permissions: what
826
+ * managing principals needs. The platform holds a deployed ontology's manifest, not its code, and
827
+ * users, roles and API keys live in tables the ontology's own entities do not shape. The roles'
828
+ * permissions name entities this ontology does not have; they were checked when the manifest was
829
+ * built, so they are not checked again, and neither are its workflows, which it carries for their
830
+ * users. The user keeps only the properties the deployed table has.
831
+ */
832
+ export function principalsOntology(manifest) {
833
+ const builtIn = manifest.roles.find((candidate) => candidate.builtIn && candidate.name === MEMBER_ROLE);
834
+ const ontology = buildOntology({
835
+ entities: [],
836
+ // Managing an old deployment must not apply new SDK defaults retroactively.
837
+ feedback: false,
838
+ roles: manifest.roles
839
+ .filter((candidate) => !candidate.builtIn)
840
+ .map((candidate) => role(candidate.name, {
841
+ description: candidate.description,
842
+ permissions: roleEntries(candidate.permissions ?? [], candidate.scopes ?? []),
843
+ })),
844
+ ...(builtIn?.permissions === undefined
845
+ ? {}
846
+ : { member: { permissions: roleEntries(builtIn.permissions, builtIn.scopes ?? []) } }),
847
+ }, { checkPermissions: false, user: deployedUser(manifest) });
848
+ // The workflows' roles were checked with the manifest; it carries them for their users.
849
+ const roles =
850
+ // @legacy ontology.feedback-unset-manifests
851
+ manifest.feedback?.enabled === true
852
+ ? feedbackRoles({ builders: [] }, manifest.feedback.files, ontology.roles, () => { }).roles
853
+ : ontology.roles;
854
+ return {
855
+ ...ontology,
856
+ roles,
857
+ manifest: buildManifest({
858
+ ...ontology,
859
+ roles,
860
+ ...(manifest.feedback === undefined ? {} : { feedback: manifest.feedback }),
861
+ }),
862
+ workflows: manifest.workflows ?? [],
863
+ };
864
+ }
865
+ const ApiKeyRow = Schema.Struct({
866
+ id: Schema.String,
867
+ key_id: Schema.String,
868
+ label: Schema.String,
869
+ created_at: Schema.String,
870
+ revoked_at: Schema.NullOr(Schema.String),
871
+ user_id: Schema.String,
872
+ user_name: Schema.String,
873
+ user_active: Schema.Boolean,
874
+ roles: Schema.Array(Schema.String),
875
+ });
876
+ const decodeApiKeyRow = Schema.decodeUnknownSync(ApiKeyRow);
877
+ function apiKeySummary(raw) {
878
+ const row = decodeApiKeyRow(raw);
879
+ return {
880
+ id: row.id,
881
+ keyId: row.key_id,
882
+ label: row.label,
883
+ createdAt: row.created_at,
884
+ revokedAt: row.revoked_at ?? undefined,
885
+ user: { id: row.user_id, name: row.user_name, active: row.user_active },
886
+ roles: row.roles.filter((assigned) => toSnakeCase(assigned) !== FEEDBACK_REPORTER_ROLE),
887
+ };
888
+ }
889
+ /** The roles assigned to the user `"u"`, sorted, as one text array. */
890
+ const ROLES_OF_USER = `COALESCE((SELECT array_agg("r"."role" ORDER BY "r"."role") FROM "${USER_ROLES_TABLE}" AS "r" ` +
891
+ `WHERE "r"."user_id" = "u"."id"), ARRAY[]::text[]) AS "roles"`;
892
+ /** Timestamps and arrays arrive as text so that every driver and database reads the same. */
893
+ const API_KEY_COLUMNS = `"k"."id"::text AS "id", "k"."key_id" AS "key_id", "k"."label" AS "label", ` +
894
+ `to_char("k"."created_at" AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS "created_at", ` +
895
+ `to_char("k"."revoked_at" AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS "revoked_at", ` +
896
+ `"u"."id"::text AS "user_id", "u"."name" AS "user_name", "u"."active" AS "user_active", ` +
897
+ ROLES_OF_USER;
898
+ const API_KEY_FROM = `FROM "${API_KEYS_TABLE}" AS "k" JOIN "${USER_TABLE}" AS "u" ON "u"."id" = "k"."user_id"`;
899
+ /** Every API key, oldest first, revoked ones included. */
900
+ export function listApiKeys() {
901
+ return Effect.gen(function* () {
902
+ const sql = yield* SqlClient.SqlClient;
903
+ const rows = yield* sql.unsafe(`SELECT ${API_KEY_COLUMNS} ${API_KEY_FROM} ORDER BY "k"."created_at", "k"."id"`);
904
+ return rows.map(apiKeySummary);
905
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
906
+ }
907
+ /** One API key by its id, or undefined. */
908
+ export function findApiKey(input) {
909
+ return Effect.gen(function* () {
910
+ if (!isRecordId(input.id)) {
911
+ return undefined;
912
+ }
913
+ const sql = yield* SqlClient.SqlClient;
914
+ const [row] = yield* sql.unsafe(`SELECT ${API_KEY_COLUMNS} ${API_KEY_FROM} WHERE "k"."id" = $1::uuid`, [input.id]);
915
+ return row === undefined ? undefined : apiKeySummary(row);
916
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
917
+ }
918
+ const MemberRow = Schema.Struct({
919
+ id: Schema.String,
920
+ email: Schema.NullOr(Schema.String),
921
+ name: Schema.String,
922
+ active: Schema.Boolean,
923
+ signed_in: Schema.Boolean,
924
+ blocked: Schema.optionalKey(Schema.Boolean),
925
+ identities: Schema.optionalKey(Schema.Array(Schema.Struct({
926
+ provider: Schema.String,
927
+ subject: Schema.String,
928
+ username: Schema.NullOr(Schema.String),
929
+ }))),
930
+ roles: Schema.Array(Schema.String),
931
+ last_sign_in_at: Schema.NullOr(Schema.String),
932
+ });
933
+ const decodeMemberRow = Schema.decodeUnknownSync(MemberRow);
934
+ function memberStatus(row) {
935
+ if (row.blocked) {
936
+ return "inactive";
937
+ }
938
+ if (!row.active) {
939
+ return "inactive";
940
+ }
941
+ return row.signed_in ? "active" : "invited";
942
+ }
943
+ function memberSummary(raw) {
944
+ const row = decodeMemberRow(raw);
945
+ return {
946
+ id: row.id,
947
+ email: row.email ?? undefined,
948
+ name: row.name,
949
+ active: row.active && !row.blocked,
950
+ ...(row.blocked ? { blocked: true } : {}),
951
+ ...(row.identities?.length
952
+ ? {
953
+ identities: row.identities.map(({ provider, subject, username }) => ({
954
+ provider,
955
+ subject,
956
+ ...(username === null ? {} : { username }),
957
+ })),
958
+ }
959
+ : {}),
960
+ status: memberStatus(row),
961
+ roles: row.roles.filter((assigned) => assigned !== MEMBER_ROLE && toSnakeCase(assigned) !== FEEDBACK_REPORTER_ROLE),
962
+ lastSignInAt: row.last_sign_in_at ?? undefined,
963
+ };
964
+ }
965
+ /**
966
+ * `last_sign_in_at` is read through the row's JSON rather than as a column: the platform lists the
967
+ * @legacy ontology.pre-upgrade-database-tolerance
968
+ * members of workspaces deployed with an earlier SDK too, whose `user` table has no such column
969
+ * until their next deploy, and those members then have no last sign-in instead of failing the list.
970
+ */
971
+ const LAST_SIGN_IN_AT = `to_char((to_jsonb("u") ->> 'last_sign_in_at')::timestamptz AT TIME ZONE 'UTC', ` +
972
+ `'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS "last_sign_in_at"`;
973
+ /**
974
+ * Platform reads also serve databases deployed before channel identities existed, and before
975
+ * `user_identity` did: those read the legacy stores.
976
+ */
977
+ function memberColumns(sql) {
978
+ return Effect.gen(function* () {
979
+ const [tables] = yield* sql.unsafe(`SELECT to_regclass($1) IS NOT NULL AS "identities"`, [AGENT_IDENTITIES_TABLE]);
980
+ const recorded = yield* userIdentitiesAvailable(sql);
981
+ // Revocation removes channel access, but the member has still joined and is no longer invited.
982
+ const channelMember = tables?.identities
983
+ ? ` OR EXISTS (SELECT 1 FROM "${AGENT_IDENTITIES_TABLE}" AS "i" WHERE "i"."user_id" = "u"."id")`
984
+ : "";
985
+ const signedIn = recorded
986
+ ? `EXISTS (SELECT 1 FROM "${USER_IDENTITY_TABLE}" AS "i" WHERE "i"."user_id" = "u"."id")`
987
+ : `("u"."identity_subject" IS NOT NULL${channelMember})`;
988
+ // The member's live channel accounts; sign-in identities are not listed here.
989
+ const identities = recorded
990
+ ? `COALESCE((
991
+ SELECT jsonb_agg(
992
+ jsonb_build_object('provider', i."provider", 'subject', i."subject",
993
+ 'username', i."username") ORDER BY i."provider", i."subject"
994
+ )
995
+ FROM ${tables?.identities ? CHANNEL_IDENTITIES_FROM : `"${USER_IDENTITY_TABLE}" AS "i"`}
996
+ WHERE i."user_id" = u."id"
997
+ AND ${tables?.identities ? LIVE_CHANNEL_IDENTITY : LIVE_CHANNEL_RECORD}
998
+ ), '[]'::jsonb)`
999
+ : tables?.identities
1000
+ ? `COALESCE((
1001
+ SELECT jsonb_agg(
1002
+ jsonb_build_object(
1003
+ 'provider', i."provider",
1004
+ 'subject', i."external_id",
1005
+ 'username', to_jsonb(i)->>'username'
1006
+ ) ORDER BY i."provider", i."external_id"
1007
+ )
1008
+ FROM "${AGENT_IDENTITIES_TABLE}" i
1009
+ WHERE i."user_id" = u."id" AND i."revoked_at" IS NULL
1010
+ ), '[]'::jsonb)`
1011
+ : "'[]'::jsonb";
1012
+ const blocked = (yield* channelUsersAvailable(sql))
1013
+ ? `EXISTS (SELECT 1 FROM "${CHANNEL_USERS_TABLE}" c WHERE c."user_id" = u."id" AND c."blocked_at" IS NOT NULL)`
1014
+ : "false";
1015
+ return (`${blocked} AS "blocked", ${identities} AS "identities", ` +
1016
+ `"u"."id"::text AS "id", "u"."email" AS "email", "u"."name" AS "name", ` +
1017
+ `"u"."active" AS "active", ` +
1018
+ `${signedIn} AS "signed_in", ${ROLES_OF_USER}, ` +
1019
+ LAST_SIGN_IN_AT);
1020
+ });
1021
+ }
1022
+ const MEMBER_FROM = `FROM "${USER_TABLE}" AS "u" WHERE "u"."kind" = 'member'`;
1023
+ /** Every member, signed in or not, by email. */
1024
+ export function listMembers() {
1025
+ return Effect.gen(function* () {
1026
+ const sql = yield* SqlClient.SqlClient;
1027
+ const columns = yield* memberColumns(sql);
1028
+ const rows = yield* sql.unsafe(`SELECT ${columns} ${MEMBER_FROM} ORDER BY "u"."email" NULLS LAST, "u"."id"`);
1029
+ return rows.map(memberSummary);
1030
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
1031
+ }
1032
+ /** Emails are stored lowercased, as sign-in links them. */
1033
+ function normalizeEmail(email) {
1034
+ return email.trim().toLowerCase();
1035
+ }
1036
+ const SignInRow = Schema.Struct({
1037
+ id: Schema.String,
1038
+ active: Schema.Boolean,
1039
+ neon_auth: Schema.Boolean,
1040
+ });
1041
+ const decodeSignInRow = Schema.decodeUnknownSync(SignInRow);
1042
+ /**
1043
+ * Whether a person may sign in with this address, and how; it reads and changes nothing else.
1044
+ * Under `invited`, an active member who signed in with Neon Auth before gets `code`, an active
1045
+ * member who has not gets `link`, and anyone else is not allowed: a deactivated member is answered
1046
+ * like an unknown address. Under `open`, unblocked addresses get `link`.
1047
+ */
1048
+ export function checkSignIn(ontology, email) {
1049
+ return Effect.gen(function* () {
1050
+ const address = normalizeEmail(email);
1051
+ if (!EMAIL_PATTERN.test(address)) {
1052
+ return { allowed: false };
1053
+ }
1054
+ const sql = yield* SqlClient.SqlClient;
1055
+ // Neon Auth accounts receive sign-in codes: the member's live email sign-in identity.
1056
+ const neonAuth = (yield* userIdentitiesAvailable(sql))
1057
+ ? `EXISTS (SELECT 1 FROM "${USER_IDENTITY_TABLE}" AS "i" WHERE "i"."user_id" = "u"."id" ` +
1058
+ `AND "i"."provider" = $2 AND "i"."origin"::text IN ${SIGN_IN_ORIGIN_SQL} ` +
1059
+ `AND "i"."revoked_at" IS NULL)`
1060
+ : `("u"."identity_provider" = $2 AND "u"."identity_subject" IS NOT NULL)`;
1061
+ const [row] = yield* sql.unsafe(`SELECT "u"."id"::text AS "id", "u"."active" AS "active", ${neonAuth} AS "neon_auth" ` +
1062
+ `FROM "${USER_TABLE}" AS "u" WHERE "u"."kind" = 'member' AND "u"."email" = $1 LIMIT 1`, [address, NEON_AUTH_PROVIDER]);
1063
+ if (row === undefined) {
1064
+ return ontology.signIn.policy === "open"
1065
+ ? { allowed: true, method: "link" }
1066
+ : { allowed: false };
1067
+ }
1068
+ const member = decodeSignInRow(row);
1069
+ if (yield* isMemberBlocked(sql, member.id)) {
1070
+ return { allowed: false };
1071
+ }
1072
+ if (ontology.signIn.policy === "open") {
1073
+ return { allowed: true, method: "link" };
1074
+ }
1075
+ if (!member.active) {
1076
+ return { allowed: false };
1077
+ }
1078
+ return { allowed: true, method: member.neon_auth ? "code" : "link" };
1079
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
1080
+ }
1081
+ function memberNotFound(key) {
1082
+ const described = "id" in key ? `the id ${key.id}` : `the email ${normalizeEmail(key.email)}`;
1083
+ return new MemberNotFound({ message: `No member has ${described}.` });
1084
+ }
1085
+ /** The user with this id or email, locked until the transaction ends, members and services alike. */
1086
+ function lockUser(runtime, key) {
1087
+ if ("id" in key) {
1088
+ if (!isRecordId(key.id)) {
1089
+ return Effect.succeed(undefined);
1090
+ }
1091
+ return runtime
1092
+ .get({ entity: "user", id: key.id, forUpdate: true })
1093
+ .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
1094
+ }
1095
+ return runtime
1096
+ .find({
1097
+ entity: "user",
1098
+ options: { where: { email: normalizeEmail(key.email) }, limit: 1, forUpdate: true },
1099
+ })
1100
+ .pipe(Effect.map(([user]) => user));
1101
+ }
1102
+ function readMember(sql, id) {
1103
+ return Effect.gen(function* () {
1104
+ const columns = yield* memberColumns(sql);
1105
+ const [row] = yield* sql.unsafe(`SELECT ${columns} ${MEMBER_FROM} AND "u"."id" = $1::uuid`, [
1106
+ id,
1107
+ ]);
1108
+ if (row === undefined) {
1109
+ return yield* Effect.die(new Error(`Member ${id} was written but cannot be read back.`));
1110
+ }
1111
+ return memberSummary(row);
1112
+ });
1113
+ }
1114
+ /**
1115
+ * Adds and removes a member's roles in one transaction. Creates a member only for an unknown email
1116
+ * when `grant` is given. Granting a role the member holds, or revoking one they do not, changes
1117
+ * nothing. A member created here is linked to their identity by email on first sign-in. The writes
1118
+ * are the `user` actions' own, as platform writes: history names `user.invite`,
1119
+ * `user.revoke_roles` and `user.grant_roles`, without a principal.
1120
+ */
1121
+ export function updateMemberRoles(ontology, change) {
1122
+ return Effect.gen(function* () {
1123
+ const sql = yield* SqlClient.SqlClient;
1124
+ const key = "id" in change ? { id: change.id } : { email: normalizeEmail(change.email) };
1125
+ const grant = change.grant === undefined ? undefined : [...new Set(change.grant)];
1126
+ const revoke = change.revoke === "all" ? "all" : [...new Set(change.revoke ?? [])];
1127
+ const declared = ontology.roles.map((candidate) => candidate.name);
1128
+ // Refused before anything is read, whoever the member is.
1129
+ yield* requireGrantable(declared, grant ?? []);
1130
+ yield* refuseMember(revoke === "all" ? [] : revoke);
1131
+ // One call is one platform write: its rows share an action id, each under its own action.
1132
+ const actionId = randomUUID();
1133
+ const runtime = (action) => platformRuntime(ontology, sql, action, actionId);
1134
+ const attempt = sql.withTransaction(Effect.gen(function* () {
1135
+ yield* beginReplicationWrite(sql);
1136
+ const found = yield* lockUser(runtime(MEMBER_HISTORY_ACTIONS.invite), key);
1137
+ if (found !== undefined && found.kind !== "member") {
1138
+ return yield* new MemberNotFound({
1139
+ message: `${"id" in key ? key.id : key.email} belongs to a service, not a member.`,
1140
+ });
1141
+ }
1142
+ // Only the email path creates invitations; an unknown id must never create a user.
1143
+ if (found === undefined) {
1144
+ if ("id" in key || grant === undefined) {
1145
+ return yield* memberNotFound(key);
1146
+ }
1147
+ const invited = yield* runtime(MEMBER_HISTORY_ACTIONS.invite).members.invite({
1148
+ email: key.email,
1149
+ name: change.name,
1150
+ roles: grant,
1151
+ });
1152
+ return yield* readMember(sql, invited.id);
1153
+ }
1154
+ const userId = found.id;
1155
+ const granted = grant ?? [];
1156
+ // A role in both lists stays assigned: it is not revoked, so it is not granted anew.
1157
+ const held = revoke === "all" ? yield* assignedRoles(sql, userId) : revoke;
1158
+ yield* runtime(MEMBER_HISTORY_ACTIONS.revokeRoles).members.revokeRoles({
1159
+ id: userId,
1160
+ roles: held.filter((role) => !granted.includes(role)),
1161
+ });
1162
+ yield* runtime(MEMBER_HISTORY_ACTIONS.grantRoles).members.grantRoles({
1163
+ id: userId,
1164
+ roles: granted,
1165
+ });
1166
+ return yield* readMember(sql, userId);
1167
+ }));
1168
+ // Two first grants to one email race to create the member; the loser finds it on retry.
1169
+ return yield* attempt.pipe(Effect.catchTag("Conflict", () => attempt), Effect.catchTag(["Conflict", "NotFound"], (error) => Effect.die(error)));
1170
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
1171
+ }
1172
+ /**
1173
+ * Grants roles of the ontology to the member with this email, creating the member when no user has
1174
+ * it; their name defaults to the email. Granting a role they hold changes nothing.
1175
+ */
1176
+ export function grantMemberRoles(ontology, input) {
1177
+ return updateMemberRoles(ontology, { email: input.email, name: input.name, grant: input.roles });
1178
+ }
1179
+ /**
1180
+ * Removes roles from the member with this email, or every assigned role. The member stays, so
1181
+ * history keeps their name. Roles the ontology no longer declares can be removed too.
1182
+ */
1183
+ export function revokeMemberRoles(ontology, input) {
1184
+ return updateMemberRoles(ontology, { email: input.email, revoke: input.roles });
1185
+ }
1186
+ /**
1187
+ * Deactivates or reactivates a member, as `user.deactivate` and `user.reactivate` do, as a platform
1188
+ * write. An inactive member keeps their roles and history, and the service rejects their requests.
1189
+ * Deactivation also revokes sessions, including repeated requests.
1190
+ */
1191
+ export function setMemberActive(ontology, input) {
1192
+ return Effect.gen(function* () {
1193
+ const sql = yield* SqlClient.SqlClient;
1194
+ const action = input.active
1195
+ ? MEMBER_HISTORY_ACTIONS.reactivate
1196
+ : MEMBER_HISTORY_ACTIONS.deactivate;
1197
+ const runtime = platformRuntime(ontology, sql, action);
1198
+ return yield* sql.withTransaction(Effect.gen(function* () {
1199
+ yield* beginReplicationWrite(sql);
1200
+ const user = yield* lockUser(runtime, input);
1201
+ if (user === undefined || user.kind !== "member") {
1202
+ return yield* memberNotFound(input);
1203
+ }
1204
+ const id = user.id;
1205
+ yield* runtime.members.setActive({ id, active: input.active });
1206
+ return yield* readMember(sql, id);
1207
+ }));
1208
+ }).pipe(Effect.catchTag("NotFound", (error) => Effect.die(error)), Effect.catchIf(isSqlError, (error) => Effect.die(error)));
1209
+ }
1210
+ export function isMemberBlocked(sql, userId) {
1211
+ return Effect.gen(function* () {
1212
+ if (!(yield* channelUsersAvailable(sql))) {
1213
+ return false;
1214
+ }
1215
+ const [blocked] = yield* sql.unsafe(`SELECT 1 FROM "${CHANNEL_USERS_TABLE}" WHERE "user_id" = $1::uuid AND "blocked_at" IS NOT NULL`, [userId]);
1216
+ return blocked !== undefined;
1217
+ });
1218
+ }
1219
+ /** Must run in the issuance transaction; the shared User lock serializes insertion with revocation. */
1220
+ export function lockSessionMember(sql, userId) {
1221
+ return Effect.gen(function* () {
1222
+ const [user] = yield* sql.unsafe(`SELECT "kind"::text AS "kind", "active" FROM "${USER_TABLE}"
1223
+ WHERE "id" = $1::uuid FOR SHARE`, [userId]);
1224
+ if (user?.kind !== "member" || !user.active || (yield* isMemberBlocked(sql, userId))) {
1225
+ return yield* Effect.die(new PrincipalRejected({ reason: "inactive_user", message: "inactive_user" }));
1226
+ }
1227
+ });
1228
+ }
1229
+ /** Sticky blocking is independent of activation and survives unpairing or another identity. */
1230
+ export function setMemberBlocked(ontology, input) {
1231
+ return Effect.gen(function* () {
1232
+ const sql = yield* SqlClient.SqlClient;
1233
+ const runtime = platformRuntime(ontology, sql, input.blocked ? "user.block" : "user.unblock");
1234
+ return yield* sql.withTransaction(Effect.gen(function* () {
1235
+ yield* beginReplicationWrite(sql);
1236
+ const user = yield* lockUser(runtime, { id: input.id });
1237
+ if (user === undefined || user.kind !== "member") {
1238
+ return yield* memberNotFound({ id: input.id });
1239
+ }
1240
+ // A member who never signed in by email or SegmentStream was invited.
1241
+ const signedIn = (yield* userIdentitiesAvailable(sql))
1242
+ ? (yield* liveSignInIdentities(sql, input.id)).length > 0
1243
+ : user.identity_subject !== null;
1244
+ yield* sql.unsafe(`INSERT INTO "${CHANNEL_USERS_TABLE}" ("user_id", "created_via", "blocked_at")
1245
+ VALUES ($1::uuid, $2, CASE WHEN $3 THEN now() ELSE NULL END)
1246
+ ON CONFLICT ("user_id") DO UPDATE SET "blocked_at" = EXCLUDED."blocked_at"`, [input.id, signedIn ? "email" : "invite", input.blocked]);
1247
+ if (input.blocked) {
1248
+ yield* revokeMemberSessions(sql, input.id);
1249
+ }
1250
+ return yield* readMember(sql, input.id);
1251
+ }));
1252
+ }).pipe(Effect.catchIf(isSqlError, (error) => Effect.die(error)));
1253
+ }
1254
+ /** Active admitted OIDC members who would lose browser access if email were their only method. */
1255
+ export function countUsersWithoutEmailIdentity() {
1256
+ return Effect.gen(function* () {
1257
+ const sql = yield* SqlClient.SqlClient;
1258
+ const [table] = yield* sql.unsafe(`SELECT to_regclass('_user_identities') IS NOT NULL AS present,
1259
+ to_regclass('"user"') IS NOT NULL AS "usersPresent"`);
1260
+ if (!table?.usersPresent) {
1261
+ return 0;
1262
+ }
1263
+ if (yield* userIdentitiesAvailable(sql)) {
1264
+ const [row] = yield* sql.unsafe(`SELECT count(*)::int AS count FROM "user" u WHERE u.kind = 'member' AND u.active = true
1265
+ AND ${liveIdentitySql(OIDC_PROVIDER)} AND NOT ${liveIdentitySql(NEON_AUTH_PROVIDER)}`);
1266
+ return row?.count ?? 0;
1267
+ }
1268
+ if (!table.present) {
1269
+ return 0;
1270
+ }
1271
+ const [row] = yield* sql.unsafe(`SELECT count(*)::int AS count FROM "user" u
1272
+ WHERE u.kind = 'member' AND u.active = true
1273
+ AND EXISTS (SELECT 1 FROM "_user_identities" i WHERE i.user_id = u.id)
1274
+ AND (u.identity_provider IS DISTINCT FROM 'neon-auth' OR u.identity_subject IS NULL)`);
1275
+ return row?.count ?? 0;
1276
+ });
1277
+ }
1278
+ /** The member `u` holds a live sign-in identity of the provider, `$1` its issuer when given. */
1279
+ function liveIdentitySql(provider, issuer) {
1280
+ return (`EXISTS (SELECT 1 FROM "${USER_IDENTITY_TABLE}" i WHERE i.user_id = u.id ` +
1281
+ `AND i.provider = '${provider}' AND i.origin::text IN ${SIGN_IN_ORIGIN_SQL} ` +
1282
+ `AND i.revoked_at IS NULL${issuer === undefined ? "" : ` AND i.issuer = ${issuer}`})`);
1283
+ }
1284
+ /** Established email members who would lose access if SegmentStream became their only method. */
1285
+ export function countUsersWithoutSegmentStreamIdentity(issuer) {
1286
+ return Effect.gen(function* () {
1287
+ const sql = yield* SqlClient.SqlClient;
1288
+ const [table] = yield* sql.unsafe(`SELECT to_regclass('_user_identities') IS NOT NULL AS present,
1289
+ to_regclass('"user"') IS NOT NULL AS "usersPresent"`);
1290
+ if (!table?.usersPresent) {
1291
+ return 0;
1292
+ }
1293
+ if (yield* userIdentitiesAvailable(sql)) {
1294
+ const [row] = yield* sql.unsafe(`SELECT count(*)::int AS count FROM "user" u WHERE u.kind = 'member' AND u.active = true
1295
+ AND ${liveIdentitySql(NEON_AUTH_PROVIDER)}
1296
+ AND NOT ${liveIdentitySql(OIDC_PROVIDER, issuer === undefined ? undefined : "$1")}`, issuer === undefined ? [] : [issuer]);
1297
+ return row?.count ?? 0;
1298
+ }
1299
+ const missing = table.present
1300
+ ? `AND NOT EXISTS (SELECT 1 FROM "_user_identities" i
1301
+ WHERE i.user_id = u.id ${issuer === undefined ? "" : "AND i.issuer = $1"})`
1302
+ : "";
1303
+ const [row] = yield* sql.unsafe(`SELECT count(*)::int AS count FROM "user" u WHERE u.kind = 'member' AND u.active = true
1304
+ AND u.identity_provider = 'neon-auth' AND u.identity_subject IS NOT NULL ${missing}`, table?.present && issuer !== undefined ? [issuer] : []);
1305
+ return row?.count ?? 0;
1306
+ });
1307
+ }