@kky42/pievo-server 2.4.12 → 2.4.14

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 (88) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/SignIn-CIFwnZtF.js +1 -0
  3. package/.output/public/assets/{ui-s5IMg6RF.js → actionUi-5PtES4q0.js} +1 -1
  4. package/.output/public/assets/app-DvHMhs7S.css +2 -0
  5. package/.output/public/assets/index-CRX2pu0W.js +10 -0
  6. package/.output/public/assets/loops._loopId-BsuV-Rmf.js +67 -0
  7. package/.output/public/assets/loops._loopId_.runs._runId-DpK_fzmZ.js +3 -0
  8. package/.output/public/assets/{preload-helper-Bs7oCAsw.js → preload-helper-sAZOZb3G.js} +3 -3
  9. package/.output/public/assets/routes-Dm_3J2aV.js +1 -0
  10. package/.output/public/assets/routes-DxF_s3cD.js +1 -0
  11. package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
  12. package/.output/server/_ssr/{MachinesModal-BhRw7oOx.mjs → MachinesModal-BaEtzycr.mjs} +24 -57
  13. package/.output/server/_ssr/{SignIn-BhoZIOUp.mjs → SignIn-sHQaBQJC.mjs} +3 -3
  14. package/.output/server/_ssr/{ui-DGz-q9MT.mjs → actionUi-5HOCZnom.mjs} +115 -4
  15. package/.output/server/_ssr/{artifactFiles-ChuJ4PzC.mjs → artifactFiles-DO43wwXA.mjs} +4 -3
  16. package/.output/server/_ssr/{artifactKind-DXumOv7c.mjs → artifactKind-DwDr8IYc.mjs} +6 -14
  17. package/.output/server/_ssr/artifacts-Cd7Qx3Qp.mjs +2 -0
  18. package/.output/server/_ssr/{auth-LwPp_iim.mjs → auth-0AamrwHo.mjs} +44 -124
  19. package/.output/server/_ssr/{SocialLinks-DVCLmql0.mjs → auth-client-lSMb_Idt.mjs} +143 -109
  20. package/.output/server/_ssr/boot-C472kLwY.mjs +3947 -0
  21. package/.output/server/_ssr/{bun-sqlite-dialect-BW9W1_Ps-CHMDkKqA.mjs → bun-sqlite-dialect-BW9W1_Ps-DLIXocA0.mjs} +2 -2
  22. package/.output/server/_ssr/{d1-sqlite-dialect-BLC8LXE6-DYum6qky.mjs → d1-sqlite-dialect-BLC8LXE6-BoFMIPr_.mjs} +2 -2
  23. package/.output/server/_ssr/db-BzvMQ2kx.mjs +2 -0
  24. package/.output/server/_ssr/{dist-bWqs5sTy.mjs → dist-ClzqkwxD.mjs} +2 -2
  25. package/.output/server/_ssr/{factory-pBwrjVTI.mjs → factory-ixUjZqJn.mjs} +2 -2
  26. package/.output/server/_ssr/{kysely-adapter-DMf5pFTf.mjs → kysely-adapter-DkZbmKgV.mjs} +1 -1
  27. package/.output/server/_ssr/logger-DDjsYB2X.mjs +2 -0
  28. package/.output/server/_ssr/{loopApi-Im6JSK0p.mjs → loopApi-DT751TJA.mjs} +30 -90
  29. package/.output/server/_ssr/{loopProjection-DIpGR6rp.mjs → loopProjection-qaD2I48a.mjs} +5 -2
  30. package/.output/server/_ssr/loopTags-CXx5UdGf.mjs +3356 -0
  31. package/.output/server/_ssr/{loops._loopId-B5iPseDG.mjs → loops._loopId-CXgw2Rz1.mjs} +144 -46
  32. package/.output/server/_ssr/{loops._loopId_.runs._runId-DVtG0xYb.mjs → loops._loopId_.runs._runId-Dk94Z0Jq.mjs} +6 -13
  33. package/.output/server/_ssr/{loops._loopId_.runs._runId-BA-P6sGA.mjs → loops._loopId_.runs._runId-SASSqOfy.mjs} +31 -23
  34. package/.output/server/_ssr/{machineFns-Bb9KNXun.mjs → machineFns-BSrIgn3f.mjs} +42 -108
  35. package/.output/server/_ssr/{node-sqlite-dialect-Dho9YjXb.mjs → node-sqlite-dialect-BbUqnnYv.mjs} +2 -2
  36. package/.output/server/_ssr/{router-C8iotXtk.mjs → router-BlLlhfJe.mjs} +26 -40
  37. package/.output/server/_ssr/routes-Cr-9yDEH.mjs +491 -0
  38. package/.output/server/_ssr/{routes-FO0MFfYK.mjs → routes-DRBS4Zi4.mjs} +4 -9
  39. package/.output/server/_ssr/routes-DTBltabU.mjs +27 -0
  40. package/.output/server/_ssr/ssr.mjs +28 -88
  41. package/.output/server/_ssr/store-vDZ9INSa.mjs +2 -0
  42. package/.output/server/_ssr/tokens-BHiCElYD.mjs +2 -0
  43. package/.output/server/_tanstack-start-manifest_v-DxLIQ0cc.mjs +52 -0
  44. package/.output/server/drizzle/0002_remove_teams.sql +15 -0
  45. package/.output/server/drizzle/0003_add_loop_tags.sql +5 -0
  46. package/.output/server/drizzle/meta/_journal.json +14 -0
  47. package/.output/server/index.mjs +47 -96
  48. package/drizzle/0002_remove_teams.sql +15 -0
  49. package/drizzle/0003_add_loop_tags.sql +5 -0
  50. package/drizzle/meta/_journal.json +14 -0
  51. package/package.json +2 -2
  52. package/.output/public/assets/MachinesModal-ByHejech.js +0 -1
  53. package/.output/public/assets/SignIn-Cf4HOpdF.js +0 -1
  54. package/.output/public/assets/actionUi-yYVvoOTn.js +0 -1
  55. package/.output/public/assets/app-D5Hy4BcS.css +0 -2
  56. package/.output/public/assets/index-D504r3X9.js +0 -10
  57. package/.output/public/assets/invite._token-B0EKVyrU.js +0 -1
  58. package/.output/public/assets/invite._token-BSnGRyUw.js +0 -1
  59. package/.output/public/assets/lifecycleUi-C0DaC6NM.js +0 -1
  60. package/.output/public/assets/loops._loopId-j5EB2PLW.js +0 -67
  61. package/.output/public/assets/loops._loopId_.runs._runId-BICo_pRz.js +0 -3
  62. package/.output/public/assets/routes-4N6mrQK_.js +0 -1
  63. package/.output/public/assets/routes-DZU0Tqka.js +0 -1
  64. package/.output/public/assets/t._teamId-CAUjvMZ6.js +0 -1
  65. package/.output/public/assets/t._teamId-Cqzd69sO.js +0 -1
  66. package/.output/server/_ssr/DashboardView-DrKNkmcX.mjs +0 -867
  67. package/.output/server/_ssr/actionUi-Cf6DuEC2.mjs +0 -119
  68. package/.output/server/_ssr/artifacts-DOLHJHcd.mjs +0 -2
  69. package/.output/server/_ssr/boot-BIxtZHU-.mjs +0 -7466
  70. package/.output/server/_ssr/boot-D8cj4uZj.mjs +0 -2
  71. package/.output/server/_ssr/db-BphSJs66.mjs +0 -2
  72. package/.output/server/_ssr/invite._token-CPSvdDox.mjs +0 -27
  73. package/.output/server/_ssr/invite._token-CRBsTSTZ.mjs +0 -84
  74. package/.output/server/_ssr/invite._token-CaTLt92a.mjs +0 -20
  75. package/.output/server/_ssr/lifecycleUi-DauKavgx.mjs +0 -50
  76. package/.output/server/_ssr/logger-EinA2J6y.mjs +0 -2
  77. package/.output/server/_ssr/routes-CsV1weub.mjs +0 -26
  78. package/.output/server/_ssr/routes-fiMzFtcP.mjs +0 -39
  79. package/.output/server/_ssr/runDiff-BLdot9mi.mjs +0 -2
  80. package/.output/server/_ssr/store-CFCp_ggC.mjs +0 -2
  81. package/.output/server/_ssr/t._teamId-BYA4Dnjp.mjs +0 -43
  82. package/.output/server/_ssr/t._teamId-CKsrgN86.mjs +0 -32
  83. package/.output/server/_ssr/t._teamId-D1BTwhWf.mjs +0 -31
  84. package/.output/server/_ssr/teamCookie-B7hLOIie.mjs +0 -10
  85. package/.output/server/_ssr/teamFns-DfqtuBW2.mjs +0 -451
  86. package/.output/server/_ssr/teamFns-cI_tsiCv.mjs +0 -32
  87. package/.output/server/_ssr/tokens-CUz4LDLm.mjs +0 -2
  88. package/.output/server/_tanstack-start-manifest_v-xSxVtI1J.mjs +0 -86
@@ -0,0 +1,3356 @@
1
+ import { o as __toESM } from "../_runtime.mjs";
2
+ import { t as Ve } from "../_libs/electric-sql__pglite.mjs";
3
+ import { A as boolean, C as notInArray, D as text, E as timestamp, O as jsonb, S as ne, T as pgTable, _ as isNotNull, a as uniqueIndex, b as lt, c as relations, d as and, f as eq, g as inArray, i as index, j as sql, k as integer, l as asc, n as src_default, o as check, p as gt, r as drizzle$1, t as drizzle, u as desc, v as isNull, w as or } from "../_libs/drizzle-orm+postgres.mjs";
4
+ import { t as require_pino } from "../_libs/pino+[...].mjs";
5
+ import { t as require_pino_pretty } from "../_libs/pino-pretty+[...].mjs";
6
+ import { t as E } from "../_libs/croner.mjs";
7
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
8
+ import fs from "node:fs";
9
+ import { fileURLToPath } from "node:url";
10
+ import path from "node:path";
11
+ import os from "node:os";
12
+ //#region node_modules/.nitro/vite/services/ssr/assets/loopTags-CXx5UdGf.js
13
+ var import_pino = /* @__PURE__ */ __toESM(require_pino());
14
+ var import_pino_pretty = /* @__PURE__ */ __toESM(require_pino_pretty());
15
+ function defineErrorCodes(codes) {
16
+ return Object.fromEntries(Object.entries(codes).map(([key, value]) => [key, {
17
+ code: key,
18
+ message: value,
19
+ toString: () => key
20
+ }]));
21
+ }
22
+ var BASE_ERROR_CODES = defineErrorCodes({
23
+ USER_NOT_FOUND: "User not found",
24
+ FAILED_TO_CREATE_USER: "Failed to create user",
25
+ FAILED_TO_CREATE_SESSION: "Failed to create session",
26
+ FAILED_TO_UPDATE_USER: "Failed to update user",
27
+ FAILED_TO_GET_SESSION: "Failed to get session",
28
+ INVALID_PASSWORD: "Invalid password",
29
+ INVALID_EMAIL: "Invalid email",
30
+ INVALID_EMAIL_OR_PASSWORD: "Invalid email or password",
31
+ INVALID_USER: "Invalid user",
32
+ SOCIAL_ACCOUNT_ALREADY_LINKED: "Social account already linked",
33
+ PROVIDER_NOT_FOUND: "Provider not found",
34
+ INVALID_TOKEN: "Invalid token",
35
+ TOKEN_EXPIRED: "Token expired",
36
+ ID_TOKEN_NOT_SUPPORTED: "id_token not supported",
37
+ FAILED_TO_GET_USER_INFO: "Failed to get user info",
38
+ USER_EMAIL_NOT_FOUND: "User email not found",
39
+ EMAIL_NOT_VERIFIED: "Email not verified",
40
+ PASSWORD_TOO_SHORT: "Password too short",
41
+ PASSWORD_TOO_LONG: "Password too long",
42
+ USER_ALREADY_EXISTS: "User already exists.",
43
+ USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "User already exists. Use another email.",
44
+ EMAIL_CAN_NOT_BE_UPDATED: "Email can not be updated",
45
+ CHANGE_EMAIL_DISABLED: "Change email is disabled",
46
+ CREDENTIAL_ACCOUNT_NOT_FOUND: "Credential account not found",
47
+ SESSION_EXPIRED: "Session expired. Re-authenticate to perform this action.",
48
+ FAILED_TO_UNLINK_LAST_ACCOUNT: "You can't unlink your last account",
49
+ ACCOUNT_NOT_FOUND: "Account not found",
50
+ USER_ALREADY_HAS_PASSWORD: "User already has a password. Provide that to delete the account.",
51
+ CROSS_SITE_NAVIGATION_LOGIN_BLOCKED: "Cross-site navigation login blocked. This request appears to be a CSRF attack.",
52
+ VERIFICATION_EMAIL_NOT_ENABLED: "Verification email isn't enabled",
53
+ EMAIL_ALREADY_VERIFIED: "Email is already verified",
54
+ EMAIL_MISMATCH: "Email mismatch",
55
+ SESSION_NOT_FRESH: "Session is not fresh",
56
+ LINKED_ACCOUNT_ALREADY_EXISTS: "Linked account already exists",
57
+ INVALID_ORIGIN: "Invalid origin",
58
+ INVALID_CALLBACK_URL: "Invalid callbackURL",
59
+ INVALID_REDIRECT_URL: "Invalid redirectURL",
60
+ INVALID_ERROR_CALLBACK_URL: "Invalid errorCallbackURL",
61
+ INVALID_NEW_USER_CALLBACK_URL: "Invalid newUserCallbackURL",
62
+ MISSING_OR_NULL_ORIGIN: "Missing or null Origin",
63
+ CALLBACK_URL_REQUIRED: "callbackURL is required",
64
+ FAILED_TO_CREATE_VERIFICATION: "Unable to create verification",
65
+ FIELD_NOT_ALLOWED: "Field not allowed to be set",
66
+ ASYNC_VALIDATION_NOT_SUPPORTED: "Async validation is not supported",
67
+ VALIDATION_ERROR: "Validation Error",
68
+ MISSING_FIELD: "Field is required",
69
+ METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED: "POST method requires deferSessionRefresh to be enabled in session config",
70
+ BODY_MUST_BE_AN_OBJECT: "Body must be an object",
71
+ PASSWORD_ALREADY_SET: "User already has a password set"
72
+ });
73
+ function isErrorStackTraceLimitWritable() {
74
+ const desc = Object.getOwnPropertyDescriptor(Error, "stackTraceLimit");
75
+ if (desc === void 0) return Object.isExtensible(Error);
76
+ return Object.prototype.hasOwnProperty.call(desc, "writable") ? desc.writable : desc.set !== void 0;
77
+ }
78
+ /**
79
+ * Hide internal stack frames from the error stack trace.
80
+ */
81
+ function hideInternalStackFrames(stack) {
82
+ const lines = stack.split("\n at ");
83
+ if (lines.length <= 1) return stack;
84
+ lines.splice(1, 1);
85
+ return lines.join("\n at ");
86
+ }
87
+ /**
88
+ * Creates a custom error class that hides stack frames.
89
+ */
90
+ function makeErrorForHideStackFrame(Base, clazz) {
91
+ class HideStackFramesError extends Base {
92
+ #hiddenStack;
93
+ constructor(...args) {
94
+ if (isErrorStackTraceLimitWritable()) {
95
+ const limit = Error.stackTraceLimit;
96
+ Error.stackTraceLimit = 0;
97
+ super(...args);
98
+ Error.stackTraceLimit = limit;
99
+ } else super(...args);
100
+ const stack = (/* @__PURE__ */ new Error()).stack;
101
+ if (stack) this.#hiddenStack = hideInternalStackFrames(stack.replace(/^Error/, this.name));
102
+ }
103
+ get errorStack() {
104
+ return this.#hiddenStack;
105
+ }
106
+ }
107
+ Object.defineProperty(HideStackFramesError.prototype, "constructor", {
108
+ get() {
109
+ return clazz;
110
+ },
111
+ enumerable: false,
112
+ configurable: true
113
+ });
114
+ return HideStackFramesError;
115
+ }
116
+ var statusCodes = {
117
+ OK: 200,
118
+ CREATED: 201,
119
+ ACCEPTED: 202,
120
+ NO_CONTENT: 204,
121
+ MULTIPLE_CHOICES: 300,
122
+ MOVED_PERMANENTLY: 301,
123
+ FOUND: 302,
124
+ SEE_OTHER: 303,
125
+ NOT_MODIFIED: 304,
126
+ TEMPORARY_REDIRECT: 307,
127
+ BAD_REQUEST: 400,
128
+ UNAUTHORIZED: 401,
129
+ PAYMENT_REQUIRED: 402,
130
+ FORBIDDEN: 403,
131
+ NOT_FOUND: 404,
132
+ METHOD_NOT_ALLOWED: 405,
133
+ NOT_ACCEPTABLE: 406,
134
+ PROXY_AUTHENTICATION_REQUIRED: 407,
135
+ REQUEST_TIMEOUT: 408,
136
+ CONFLICT: 409,
137
+ GONE: 410,
138
+ LENGTH_REQUIRED: 411,
139
+ PRECONDITION_FAILED: 412,
140
+ PAYLOAD_TOO_LARGE: 413,
141
+ URI_TOO_LONG: 414,
142
+ UNSUPPORTED_MEDIA_TYPE: 415,
143
+ RANGE_NOT_SATISFIABLE: 416,
144
+ EXPECTATION_FAILED: 417,
145
+ "I'M_A_TEAPOT": 418,
146
+ MISDIRECTED_REQUEST: 421,
147
+ UNPROCESSABLE_ENTITY: 422,
148
+ LOCKED: 423,
149
+ FAILED_DEPENDENCY: 424,
150
+ TOO_EARLY: 425,
151
+ UPGRADE_REQUIRED: 426,
152
+ PRECONDITION_REQUIRED: 428,
153
+ TOO_MANY_REQUESTS: 429,
154
+ REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
155
+ UNAVAILABLE_FOR_LEGAL_REASONS: 451,
156
+ INTERNAL_SERVER_ERROR: 500,
157
+ NOT_IMPLEMENTED: 501,
158
+ BAD_GATEWAY: 502,
159
+ SERVICE_UNAVAILABLE: 503,
160
+ GATEWAY_TIMEOUT: 504,
161
+ HTTP_VERSION_NOT_SUPPORTED: 505,
162
+ VARIANT_ALSO_NEGOTIATES: 506,
163
+ INSUFFICIENT_STORAGE: 507,
164
+ LOOP_DETECTED: 508,
165
+ NOT_EXTENDED: 510,
166
+ NETWORK_AUTHENTICATION_REQUIRED: 511
167
+ };
168
+ var InternalAPIError = class extends Error {
169
+ constructor(status = "INTERNAL_SERVER_ERROR", body = void 0, headers = {}, statusCode = typeof status === "number" ? status : statusCodes[status]) {
170
+ super(body?.message, body?.cause ? { cause: body.cause } : void 0);
171
+ this.status = status;
172
+ this.body = body;
173
+ this.headers = headers;
174
+ this.statusCode = statusCode;
175
+ this.name = "APIError";
176
+ this.status = status;
177
+ this.headers = headers;
178
+ this.statusCode = statusCode;
179
+ this.body = body;
180
+ }
181
+ };
182
+ var ValidationError$1 = class extends InternalAPIError {
183
+ constructor(message, issues) {
184
+ super(400, {
185
+ message,
186
+ code: "VALIDATION_ERROR"
187
+ });
188
+ this.message = message;
189
+ this.issues = issues;
190
+ this.issues = issues;
191
+ }
192
+ };
193
+ var BetterCallError = class extends Error {
194
+ constructor(message) {
195
+ super(message);
196
+ this.name = "BetterCallError";
197
+ }
198
+ };
199
+ var kAPIErrorHeaderSymbol = Symbol.for("better-call:api-error-headers");
200
+ var APIError$1 = makeErrorForHideStackFrame(InternalAPIError, Error);
201
+ var BetterAuthError = class extends Error {
202
+ constructor(message, options) {
203
+ super(message, options);
204
+ this.name = "BetterAuthError";
205
+ this.message = message;
206
+ this.stack = "";
207
+ }
208
+ };
209
+ var APIError = class APIError extends APIError$1 {
210
+ constructor(...args) {
211
+ super(...args);
212
+ }
213
+ static fromStatus(status, body) {
214
+ return new APIError(status, body);
215
+ }
216
+ static from(status, error) {
217
+ return new APIError(status, {
218
+ message: error.message,
219
+ code: error.code
220
+ });
221
+ }
222
+ };
223
+ var _envShim = Object.create(null);
224
+ var _getEnv = (useShim) => globalThis.process?.env || globalThis.Deno?.env.toObject() || globalThis.__env__ || (useShim ? _envShim : globalThis);
225
+ var env = new Proxy(_envShim, {
226
+ get(_, prop) {
227
+ return _getEnv()[prop] ?? _envShim[prop];
228
+ },
229
+ has(_, prop) {
230
+ return prop in _getEnv() || prop in _envShim;
231
+ },
232
+ set(_, prop, value) {
233
+ const env = _getEnv(true);
234
+ env[prop] = value;
235
+ return true;
236
+ },
237
+ deleteProperty(_, prop) {
238
+ if (!prop) return false;
239
+ const env = _getEnv(true);
240
+ delete env[prop];
241
+ return true;
242
+ },
243
+ ownKeys() {
244
+ const env = _getEnv(true);
245
+ return Object.keys(env);
246
+ }
247
+ });
248
+ function toBoolean(val) {
249
+ return val ? val !== "false" : false;
250
+ }
251
+ var nodeENV = env.NODE_ENV ?? "";
252
+ /** Detect if `NODE_ENV` environment variable is `production` */
253
+ var isProduction = nodeENV === "production";
254
+ /** Detect if `NODE_ENV` environment variable is `dev` or `development` */
255
+ var isDevelopment = () => nodeENV === "dev" || nodeENV === "development";
256
+ /** Detect if `NODE_ENV` environment variable is `test` */
257
+ var isTest = () => nodeENV === "test" || toBoolean(env.TEST);
258
+ /**
259
+ * Get environment variable with fallback
260
+ */
261
+ function getEnvVar(key, fallback) {
262
+ if (typeof process !== "undefined" && process.env) return process.env[key] ?? fallback;
263
+ if (typeof Deno !== "undefined") return Deno.env.get(key) ?? fallback;
264
+ if (typeof Bun !== "undefined") return Bun.env[key] ?? fallback;
265
+ return fallback;
266
+ }
267
+ /**
268
+ * Get boolean environment variable
269
+ */
270
+ function getBooleanEnvVar(key, fallback = true) {
271
+ const value = getEnvVar(key);
272
+ if (!value) return fallback;
273
+ return value !== "0" && value.toLowerCase() !== "false" && value !== "";
274
+ }
275
+ /**
276
+ * Common environment variables used in Better Auth
277
+ */
278
+ var ENV = Object.freeze({
279
+ get BETTER_AUTH_SECRET() {
280
+ return getEnvVar("BETTER_AUTH_SECRET");
281
+ },
282
+ get AUTH_SECRET() {
283
+ return getEnvVar("AUTH_SECRET");
284
+ },
285
+ get BETTER_AUTH_TELEMETRY() {
286
+ return getEnvVar("BETTER_AUTH_TELEMETRY");
287
+ },
288
+ get BETTER_AUTH_TELEMETRY_ID() {
289
+ return getEnvVar("BETTER_AUTH_TELEMETRY_ID");
290
+ },
291
+ get NODE_ENV() {
292
+ return getEnvVar("NODE_ENV", "development");
293
+ },
294
+ get PACKAGE_VERSION() {
295
+ return getEnvVar("PACKAGE_VERSION", "0.0.0");
296
+ },
297
+ get BETTER_AUTH_TELEMETRY_ENDPOINT() {
298
+ return getEnvVar("BETTER_AUTH_TELEMETRY_ENDPOINT", "");
299
+ }
300
+ });
301
+ function capitalizeFirstLetter(str) {
302
+ return str.charAt(0).toUpperCase() + str.slice(1);
303
+ }
304
+ var WORD_PATTERN = /[\p{Ll}\d]+|\p{Lu}+(?!\p{Ll})|\p{Lu}[\p{Ll}\d]+|\p{Lo}+/gu;
305
+ var APOSTROPHE_PATTERN = /['\u2019]/g;
306
+ function splitWords(input) {
307
+ return input.replace(APOSTROPHE_PATTERN, "").match(WORD_PATTERN) ?? [];
308
+ }
309
+ function toKebabCase(input) {
310
+ return splitWords(input).map((word) => word.toLowerCase()).join("-");
311
+ }
312
+ /**
313
+ * Escapes a character if it has a special meaning in regular expressions
314
+ * and returns the character as is if it doesn't
315
+ */
316
+ function escapeRegExpChar(char) {
317
+ if (char === "-" || char === "^" || char === "$" || char === "+" || char === "." || char === "(" || char === ")" || char === "|" || char === "[" || char === "]" || char === "{" || char === "}" || char === "*" || char === "?" || char === "\\") return `\\${char}`;
318
+ else return char;
319
+ }
320
+ /**
321
+ * Escapes all characters in a given string that have a special meaning in regular expressions
322
+ */
323
+ function escapeRegExpString(str) {
324
+ let result = "";
325
+ for (let i = 0; i < str.length; i++) result += escapeRegExpChar(str[i]);
326
+ return result;
327
+ }
328
+ /**
329
+ * Transforms one or more glob patterns into a RegExp pattern
330
+ */
331
+ function transform(pattern, separator = true) {
332
+ if (Array.isArray(pattern)) return `(?:${pattern.map((p) => `^${transform(p, separator)}$`).join("|")})`;
333
+ let separatorSplitter = "";
334
+ let separatorMatcher = "";
335
+ let wildcard = ".";
336
+ if (separator === true) {
337
+ separatorSplitter = "/";
338
+ separatorMatcher = "[/\\\\]";
339
+ wildcard = "[^/\\\\]";
340
+ } else if (separator) {
341
+ separatorSplitter = separator;
342
+ separatorMatcher = escapeRegExpString(separatorSplitter);
343
+ if (separatorMatcher.length > 1) {
344
+ separatorMatcher = `(?:${separatorMatcher})`;
345
+ wildcard = `((?!${separatorMatcher}).)`;
346
+ } else wildcard = `[^${separatorMatcher}]`;
347
+ }
348
+ const requiredSeparator = separator ? `${separatorMatcher}+?` : "";
349
+ const optionalSeparator = separator ? `${separatorMatcher}*?` : "";
350
+ const segments = separator ? pattern.split(separatorSplitter) : [pattern];
351
+ let result = "";
352
+ for (let s = 0; s < segments.length; s++) {
353
+ const segment = segments[s];
354
+ const nextSegment = segments[s + 1];
355
+ let currentSeparator = "";
356
+ if (!segment && s > 0) continue;
357
+ if (separator) if (s === segments.length - 1) currentSeparator = optionalSeparator;
358
+ else if (nextSegment !== "**") currentSeparator = requiredSeparator;
359
+ else currentSeparator = "";
360
+ if (separator && segment === "**") {
361
+ if (currentSeparator) {
362
+ result += s === 0 ? "" : currentSeparator;
363
+ result += `(?:${wildcard}*?${currentSeparator})*?`;
364
+ }
365
+ continue;
366
+ }
367
+ for (let c = 0; c < segment.length; c++) {
368
+ const char = segment[c];
369
+ if (char === "\\") {
370
+ if (c < segment.length - 1) {
371
+ result += escapeRegExpChar(segment[c + 1]);
372
+ c++;
373
+ }
374
+ } else if (char === "?") result += wildcard;
375
+ else if (char === "*") result += `${wildcard}*?`;
376
+ else result += escapeRegExpChar(char);
377
+ }
378
+ result += currentSeparator;
379
+ }
380
+ return result;
381
+ }
382
+ function isMatch(regexp, sample) {
383
+ if (typeof sample !== "string") throw new TypeError(`Sample must be a string, but ${typeof sample} given`);
384
+ return regexp.test(sample);
385
+ }
386
+ /**
387
+ * Compiles one or more glob patterns into a RegExp and returns an isMatch function.
388
+ * The isMatch function takes a sample string as its only argument and returns `true`
389
+ * if the string matches the pattern(s).
390
+ *
391
+ * ```js
392
+ * wildcardMatch('src/*.js')('src/index.js') //=> true
393
+ * ```
394
+ *
395
+ * ```js
396
+ * const isMatch = wildcardMatch('*.example.com', '.')
397
+ * isMatch('foo.example.com') //=> true
398
+ * isMatch('foo.bar.com') //=> false
399
+ * ```
400
+ */
401
+ function wildcardMatch(pattern, options) {
402
+ if (typeof pattern !== "string" && !Array.isArray(pattern)) throw new TypeError(`The first argument must be a single pattern string or an array of patterns, but ${typeof pattern} given`);
403
+ if (typeof options === "string" || typeof options === "boolean") options = { separator: options };
404
+ if (arguments.length === 2 && !(typeof options === "undefined" || typeof options === "object" && options !== null && !Array.isArray(options))) throw new TypeError(`The second argument must be an options object or a string/boolean separator, but ${typeof options} given`);
405
+ options = options || {};
406
+ if (options.separator === "\\") throw new Error("\\ is not a valid separator because it is used for escaping. Try setting the separator to `true` instead");
407
+ const regexpPattern = transform(pattern, options.separator);
408
+ const regexp = new RegExp(`^${regexpPattern}$`, options.flags);
409
+ const fn = isMatch.bind(null, regexp);
410
+ fn.options = options;
411
+ fn.pattern = pattern;
412
+ fn.regexp = regexp;
413
+ return fn;
414
+ }
415
+ var SLASH_CHAR_CODE = "/".charCodeAt(0);
416
+ /**
417
+ * Minimal loopback check for dev scheme inference only. Reachable from
418
+ * `client/config.ts` via `getBaseURL`, so we MUST NOT import the full
419
+ * `@better-auth/core/utils/host` classifier here: its `utils/ip` dependency
420
+ * on zod would leak into the client bundle (see `e2e/smoke/test/vite.spec.ts`).
421
+ *
422
+ * Server-side SSRF/loopback checks (oauth redirect matching, trusted-origin
423
+ * resolution, electron fetch gate) continue to use the authoritative
424
+ * `isLoopbackHost` from `@better-auth/core/utils/host`. This helper's only
425
+ * job is picking `http` vs `https` for dev ergonomics.
426
+ */
427
+ function isLoopbackForDevScheme(host) {
428
+ const hostname = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, "").toLowerCase();
429
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || hostname.startsWith("127.");
430
+ }
431
+ function trimTrailingSlashes(value) {
432
+ let end = value.length;
433
+ while (end > 0 && value.charCodeAt(end - 1) === SLASH_CHAR_CODE) end--;
434
+ return end === value.length ? value : value.slice(0, end);
435
+ }
436
+ function checkHasPath(url) {
437
+ try {
438
+ return (trimTrailingSlashes(new URL(url).pathname) || "/") !== "/";
439
+ } catch {
440
+ throw new BetterAuthError(`Invalid base URL: ${url}. Please provide a valid base URL.`);
441
+ }
442
+ }
443
+ function assertHasProtocol(url) {
444
+ try {
445
+ const parsedUrl = new URL(url);
446
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") throw new BetterAuthError(`Invalid base URL: ${url}. URL must include 'http://' or 'https://'`);
447
+ } catch (error) {
448
+ if (error instanceof BetterAuthError) throw error;
449
+ throw new BetterAuthError(`Invalid base URL: ${url}. Please provide a valid base URL.`, { cause: error });
450
+ }
451
+ }
452
+ function withPath(url, path = "/api/auth") {
453
+ assertHasProtocol(url);
454
+ if (checkHasPath(url)) return url;
455
+ const trimmedUrl = trimTrailingSlashes(url);
456
+ if (!path || path === "/") return trimmedUrl;
457
+ path = path.startsWith("/") ? path : `/${path}`;
458
+ return `${trimmedUrl}${path}`;
459
+ }
460
+ function validateProxyHeader(header, type) {
461
+ if (!header || header.trim() === "") return false;
462
+ if (type === "proto") return header === "http" || header === "https";
463
+ if (type === "host") {
464
+ if ([
465
+ /\.\./,
466
+ /\0/,
467
+ /[\s]/,
468
+ /^[.]/,
469
+ /[<>'"]/,
470
+ /javascript:/i,
471
+ /file:/i,
472
+ /data:/i
473
+ ].some((pattern) => pattern.test(header))) return false;
474
+ return /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(:[0-9]{1,5})?$/.test(header) || /^(\d{1,3}\.){3}\d{1,3}(:[0-9]{1,5})?$/.test(header) || /^\[[0-9a-fA-F:]+\](:[0-9]{1,5})?$/.test(header) || /^localhost(:[0-9]{1,5})?$/i.test(header);
475
+ }
476
+ return false;
477
+ }
478
+ function getBaseURL(url, path, request, loadEnv, trustedProxyHeaders) {
479
+ if (url) return withPath(url, path);
480
+ if (loadEnv !== false) {
481
+ const fromEnv = env.BETTER_AUTH_URL || env.NEXT_PUBLIC_BETTER_AUTH_URL || env.PUBLIC_BETTER_AUTH_URL || env.NUXT_PUBLIC_BETTER_AUTH_URL || env.NUXT_PUBLIC_AUTH_URL || (env.BASE_URL !== "/" ? env.BASE_URL : void 0);
482
+ if (fromEnv) return withPath(fromEnv, path);
483
+ }
484
+ const fromRequest = request?.headers.get("x-forwarded-host");
485
+ const fromRequestProto = request?.headers.get("x-forwarded-proto");
486
+ if (fromRequest && fromRequestProto && trustedProxyHeaders) {
487
+ if (validateProxyHeader(fromRequestProto, "proto") && validateProxyHeader(fromRequest, "host")) try {
488
+ return withPath(`${fromRequestProto}://${fromRequest}`, path);
489
+ } catch (_error) {}
490
+ }
491
+ if (request) {
492
+ const url = getOrigin(request.url);
493
+ if (!url) throw new BetterAuthError("Could not get origin from request. Please provide a valid base URL.");
494
+ return withPath(url, path);
495
+ }
496
+ if (typeof window !== "undefined" && window.location) return withPath(window.location.origin, path);
497
+ }
498
+ function getOrigin(url) {
499
+ try {
500
+ const parsedUrl = new URL(url);
501
+ return parsedUrl.origin === "null" ? null : parsedUrl.origin;
502
+ } catch {
503
+ return null;
504
+ }
505
+ }
506
+ function getProtocol(url) {
507
+ try {
508
+ return new URL(url).protocol;
509
+ } catch {
510
+ return null;
511
+ }
512
+ }
513
+ function getHost(url) {
514
+ try {
515
+ return new URL(url).host;
516
+ } catch {
517
+ return null;
518
+ }
519
+ }
520
+ /**
521
+ * Checks if the baseURL config is a dynamic config object
522
+ */
523
+ function isDynamicBaseURLConfig(config) {
524
+ return typeof config === "object" && config !== null && "allowedHosts" in config && Array.isArray(config.allowedHosts);
525
+ }
526
+ /**
527
+ * Check if a value is a `Request`
528
+ * - `instanceof`: works for native Request instances
529
+ * - `toString`: handles where instanceof check fails but the object is still a
530
+ * valid Request (e.g. cross-realm, polyfills). Paired with a shape check so
531
+ * an object that only spoofs `Symbol.toStringTag` without the real shape is
532
+ * rejected before downstream code tries to read `.headers` / `.url`.
533
+ *
534
+ * @param value The value to check
535
+ * @returns `true` if the value is a Request instance
536
+ */
537
+ function isRequestLike(value) {
538
+ if (value instanceof Request) return true;
539
+ if (typeof value !== "object" || value === null || Object.prototype.toString.call(value) !== "[object Request]") return false;
540
+ const v = value;
541
+ return typeof v.url === "string" && typeof v.headers === "object" && v.headers !== null && typeof v.headers.get === "function";
542
+ }
543
+ /**
544
+ * Extracts the host from a `Request` or `Headers`.
545
+ * Honors `x-forwarded-host` only when `trustedProxyHeaders` is enabled,
546
+ * then falls back to the `host` header and finally the request URL.
547
+ */
548
+ function getHostFromSource(source, trustedProxyHeaders) {
549
+ const headers = isRequestLike(source) ? source.headers : source;
550
+ if (trustedProxyHeaders) {
551
+ const forwardedHost = headers.get("x-forwarded-host");
552
+ if (forwardedHost && validateProxyHeader(forwardedHost, "host")) return forwardedHost;
553
+ }
554
+ const host = headers.get("host");
555
+ if (host && validateProxyHeader(host, "host")) return host;
556
+ if (isRequestLike(source)) try {
557
+ return new URL(source.url).host;
558
+ } catch {
559
+ return null;
560
+ }
561
+ return null;
562
+ }
563
+ /**
564
+ * Extracts the protocol from a `Request` or `Headers`.
565
+ * Honors `x-forwarded-proto` only when `trustedProxyHeaders` is enabled,
566
+ * then falls back to the request URL, then to "https".
567
+ */
568
+ function getProtocolFromSource(source, configProtocol, trustedProxyHeaders) {
569
+ if (configProtocol === "http" || configProtocol === "https") return configProtocol;
570
+ const headers = isRequestLike(source) ? source.headers : source;
571
+ if (trustedProxyHeaders) {
572
+ const forwardedProto = headers.get("x-forwarded-proto");
573
+ if (forwardedProto && validateProxyHeader(forwardedProto, "proto")) return forwardedProto;
574
+ }
575
+ if (isRequestLike(source)) try {
576
+ const url = new URL(source.url);
577
+ if (url.protocol === "http:" || url.protocol === "https:") return url.protocol.slice(0, -1);
578
+ } catch {}
579
+ const host = getHostFromSource(source, trustedProxyHeaders);
580
+ if (host && isLoopbackForDevScheme(host)) return "http";
581
+ return "https";
582
+ }
583
+ /**
584
+ * Matches a hostname against a host pattern.
585
+ * Supports wildcard patterns like `*.vercel.app` or `preview-*.myapp.com`.
586
+ *
587
+ * @param host The hostname to test (e.g., "myapp.com", "preview-123.vercel.app")
588
+ * @param pattern The host pattern (e.g., "myapp.com", "*.vercel.app")
589
+ * @returns {boolean} true if the host matches the pattern, false otherwise.
590
+ *
591
+ * @example
592
+ * ```ts
593
+ * matchesHostPattern("myapp.com", "myapp.com") // true
594
+ * matchesHostPattern("preview-123.vercel.app", "*.vercel.app") // true
595
+ * matchesHostPattern("preview-123.myapp.com", "preview-*.myapp.com") // true
596
+ * matchesHostPattern("evil.com", "myapp.com") // false
597
+ * ```
598
+ */
599
+ var matchesHostPattern = (host, pattern) => {
600
+ if (!host || !pattern) return false;
601
+ const normalizedHost = host.replace(/^https?:\/\//, "").split("/")[0].toLowerCase();
602
+ const normalizedPattern = pattern.replace(/^https?:\/\//, "").split("/")[0].toLowerCase();
603
+ if (normalizedPattern.includes("*") || normalizedPattern.includes("?")) return wildcardMatch(normalizedPattern)(normalizedHost);
604
+ return normalizedHost.toLowerCase() === normalizedPattern.toLowerCase();
605
+ };
606
+ /**
607
+ * Resolves the base URL from a dynamic config based on the incoming request.
608
+ * Validates the derived host against the allowedHosts allowlist.
609
+ *
610
+ * @param config The dynamic base URL config
611
+ * @param request The incoming request
612
+ * @param basePath The base path to append
613
+ * @returns The resolved base URL with path
614
+ * @throws BetterAuthError if host is not in allowedHosts and no fallback is set
615
+ */
616
+ function resolveDynamicBaseURL(config, source, basePath, trustedProxyHeaders) {
617
+ const host = getHostFromSource(source, trustedProxyHeaders);
618
+ if (!host) {
619
+ if (config.fallback) return withPath(config.fallback, basePath);
620
+ throw new BetterAuthError("Could not determine host from request headers. Please provide a fallback URL in your baseURL config.");
621
+ }
622
+ if (config.allowedHosts.some((pattern) => matchesHostPattern(host, pattern))) return withPath(`${getProtocolFromSource(source, config.protocol, trustedProxyHeaders)}://${host}`, basePath);
623
+ if (config.fallback) return withPath(config.fallback, basePath);
624
+ throw new BetterAuthError(`Host "${host}" is not in the allowed hosts list. Allowed hosts: ${config.allowedHosts.join(", ")}. Add this host to your allowedHosts config or provide a fallback URL.`);
625
+ }
626
+ /**
627
+ * Resolves the base URL from any config type (static string or dynamic object).
628
+ * This is the main entry point for base URL resolution.
629
+ *
630
+ * @param config The base URL config (string or object)
631
+ * @param basePath The base path to append
632
+ * @param request Optional request for dynamic resolution
633
+ * @param loadEnv Whether to load from environment variables
634
+ * @param trustedProxyHeaders Whether to trust proxy headers (for legacy behavior)
635
+ * @returns The resolved base URL with path
636
+ */
637
+ function resolveBaseURL(config, basePath, source, loadEnv, trustedProxyHeaders) {
638
+ if (isDynamicBaseURLConfig(config)) {
639
+ if (source) return resolveDynamicBaseURL(config, source, basePath, trustedProxyHeaders);
640
+ if (config.fallback) return withPath(config.fallback, basePath);
641
+ return getBaseURL(void 0, basePath, void 0, loadEnv, trustedProxyHeaders);
642
+ }
643
+ const request = isRequestLike(source) ? source : void 0;
644
+ if (typeof config === "string") return getBaseURL(config, basePath, request, loadEnv, trustedProxyHeaders);
645
+ return getBaseURL(void 0, basePath, request, loadEnv, trustedProxyHeaders);
646
+ }
647
+ /**
648
+ * Normalizes a request pathname by removing the basePath prefix and trailing slashes.
649
+ * This is useful for matching paths against configured path lists.
650
+ *
651
+ * @param requestUrl - The full request URL
652
+ * @param basePath - The base path of the auth API (e.g., "/api/auth")
653
+ * @returns The normalized path without basePath prefix or trailing slashes,
654
+ * or "/" if URL parsing fails
655
+ *
656
+ * @example
657
+ * normalizePathname("http://localhost:3000/api/auth/sso/saml2/callback/provider1", "/api/auth")
658
+ * // Returns: "/sso/saml2/callback/provider1"
659
+ *
660
+ * normalizePathname("http://localhost:3000/sso/saml2/callback/provider1/", "/")
661
+ * // Returns: "/sso/saml2/callback/provider1"
662
+ */
663
+ function normalizePathname(requestUrl, basePath) {
664
+ let pathname;
665
+ try {
666
+ pathname = new URL(requestUrl).pathname.replace(/\/+$/, "") || "/";
667
+ } catch {
668
+ return "/";
669
+ }
670
+ const normalizedBasePath = basePath.replace(/\/+$/, "");
671
+ if (normalizedBasePath === "") return pathname;
672
+ if (pathname === normalizedBasePath) return "/";
673
+ if (pathname.startsWith(normalizedBasePath + "/")) return pathname.slice(normalizedBasePath.length).replace(/\/+$/, "") || "/";
674
+ return pathname;
675
+ }
676
+ /**
677
+ * Schemes that execute or embed code when navigated to or accepted as a
678
+ * redirect target. These are never safe as an OAuth `redirect_uri` or as a
679
+ * client-side navigation target (`window.location.href`, `location.assign`, ...).
680
+ */
681
+ var DANGEROUS_URL_SCHEMES = [
682
+ "javascript:",
683
+ "data:",
684
+ "vbscript:"
685
+ ];
686
+ /**
687
+ * Returns `false` only when `value` is an absolute URL using a dangerous scheme
688
+ * (`javascript:`, `data:`, `vbscript:`). Relative URLs (e.g. `/dashboard`) and
689
+ * safe absolute schemes (`http`, `https`, custom app schemes such as
690
+ * `myapp://`) return `true`.
691
+ *
692
+ * Use this to guard browser navigation sinks and any redirect target that may
693
+ * originate from untrusted input. It is intentionally narrow: it blocks code
694
+ * execution schemes without rejecting relative paths or mobile deep links.
695
+ */
696
+ function isSafeUrlScheme(value) {
697
+ let parsed;
698
+ try {
699
+ parsed = new URL(value);
700
+ } catch {
701
+ return true;
702
+ }
703
+ return !DANGEROUS_URL_SCHEMES.includes(parsed.protocol);
704
+ }
705
+ function isPlainObject(value) {
706
+ if (value === null || typeof value !== "object") return false;
707
+ const prototype = Object.getPrototypeOf(value);
708
+ if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) return false;
709
+ if (Symbol.iterator in value) return false;
710
+ if (Symbol.toStringTag in value) return Object.prototype.toString.call(value) === "[object Module]";
711
+ return true;
712
+ }
713
+ function _defu(baseObject, defaults, namespace = ".", merger) {
714
+ if (!isPlainObject(defaults)) return _defu(baseObject, {}, namespace, merger);
715
+ const object = { ...defaults };
716
+ for (const key of Object.keys(baseObject)) {
717
+ if (key === "__proto__" || key === "constructor") continue;
718
+ const value = baseObject[key];
719
+ if (value === null || value === void 0) continue;
720
+ if (merger && merger(object, key, value, namespace)) continue;
721
+ if (Array.isArray(value) && Array.isArray(object[key])) object[key] = [...value, ...object[key]];
722
+ else if (isPlainObject(value) && isPlainObject(object[key])) object[key] = _defu(value, object[key], (namespace ? `${namespace}.` : "") + key.toString(), merger);
723
+ else object[key] = value;
724
+ }
725
+ return object;
726
+ }
727
+ function createDefu(merger) {
728
+ return (...arguments_) => arguments_.reduce((p, c) => _defu(p, c, "", merger), {});
729
+ }
730
+ var defu = createDefu();
731
+ var __defProp = Object.defineProperty;
732
+ var __defProps = Object.defineProperties;
733
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
734
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
735
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
736
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
737
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
738
+ enumerable: true,
739
+ configurable: true,
740
+ writable: true,
741
+ value
742
+ }) : obj[key] = value;
743
+ var __spreadValues = (a, b) => {
744
+ for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]);
745
+ if (__getOwnPropSymbols) {
746
+ for (var prop of __getOwnPropSymbols(b)) if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]);
747
+ }
748
+ return a;
749
+ };
750
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
751
+ var BetterFetchError = class extends Error {
752
+ constructor(status, statusText, error) {
753
+ super(statusText || status.toString(), { cause: error });
754
+ this.status = status;
755
+ this.statusText = statusText;
756
+ this.error = error;
757
+ Error.captureStackTrace(this, this.constructor);
758
+ }
759
+ };
760
+ var initializePlugins = async (url, options) => {
761
+ var _a, _b, _c, _d, _e, _f;
762
+ let opts = options || {};
763
+ const hooks = {
764
+ onRequest: [options == null ? void 0 : options.onRequest],
765
+ onResponse: [options == null ? void 0 : options.onResponse],
766
+ onSuccess: [options == null ? void 0 : options.onSuccess],
767
+ onError: [options == null ? void 0 : options.onError],
768
+ onRetry: [options == null ? void 0 : options.onRetry]
769
+ };
770
+ if (!options || !(options == null ? void 0 : options.plugins)) return {
771
+ url,
772
+ options: opts,
773
+ hooks
774
+ };
775
+ for (const plugin of (options == null ? void 0 : options.plugins) || []) {
776
+ if (plugin.init) {
777
+ const pluginRes = await ((_a = plugin.init) == null ? void 0 : _a.call(plugin, url.toString(), options));
778
+ opts = pluginRes.options || opts;
779
+ url = pluginRes.url;
780
+ }
781
+ hooks.onRequest.push((_b = plugin.hooks) == null ? void 0 : _b.onRequest);
782
+ hooks.onResponse.push((_c = plugin.hooks) == null ? void 0 : _c.onResponse);
783
+ hooks.onSuccess.push((_d = plugin.hooks) == null ? void 0 : _d.onSuccess);
784
+ hooks.onError.push((_e = plugin.hooks) == null ? void 0 : _e.onError);
785
+ hooks.onRetry.push((_f = plugin.hooks) == null ? void 0 : _f.onRetry);
786
+ }
787
+ return {
788
+ url,
789
+ options: opts,
790
+ hooks
791
+ };
792
+ };
793
+ var LinearRetryStrategy = class {
794
+ constructor(options) {
795
+ this.options = options;
796
+ }
797
+ shouldAttemptRetry(attempt, response) {
798
+ if (this.options.shouldRetry) return Promise.resolve(attempt < this.options.attempts && this.options.shouldRetry(response));
799
+ return Promise.resolve(attempt < this.options.attempts);
800
+ }
801
+ getDelay() {
802
+ return this.options.delay;
803
+ }
804
+ };
805
+ var ExponentialRetryStrategy = class {
806
+ constructor(options) {
807
+ this.options = options;
808
+ }
809
+ shouldAttemptRetry(attempt, response) {
810
+ if (this.options.shouldRetry) return Promise.resolve(attempt < this.options.attempts && this.options.shouldRetry(response));
811
+ return Promise.resolve(attempt < this.options.attempts);
812
+ }
813
+ getDelay(attempt) {
814
+ return Math.min(this.options.maxDelay, this.options.baseDelay * 2 ** attempt);
815
+ }
816
+ };
817
+ function createRetryStrategy(options) {
818
+ if (typeof options === "number") return new LinearRetryStrategy({
819
+ type: "linear",
820
+ attempts: options,
821
+ delay: 1e3
822
+ });
823
+ switch (options.type) {
824
+ case "linear": return new LinearRetryStrategy(options);
825
+ case "exponential": return new ExponentialRetryStrategy(options);
826
+ default: throw new Error("Invalid retry strategy");
827
+ }
828
+ }
829
+ var getAuthHeader = async (options) => {
830
+ const headers = {};
831
+ const getValue = async (value) => typeof value === "function" ? await value() : value;
832
+ if (options == null ? void 0 : options.auth) {
833
+ if (options.auth.type === "Bearer") {
834
+ const token = await getValue(options.auth.token);
835
+ if (!token) return headers;
836
+ headers["authorization"] = `Bearer ${token}`;
837
+ } else if (options.auth.type === "Basic") {
838
+ const [username, password] = await Promise.all([getValue(options.auth.username), getValue(options.auth.password)]);
839
+ if (!username || !password) return headers;
840
+ headers["authorization"] = `Basic ${btoa(`${username}:${password}`)}`;
841
+ } else if (options.auth.type === "Custom") {
842
+ const [prefix, value] = await Promise.all([getValue(options.auth.prefix), getValue(options.auth.value)]);
843
+ if (!value) return headers;
844
+ headers["authorization"] = `${prefix != null ? prefix : ""} ${value}`;
845
+ }
846
+ }
847
+ return headers;
848
+ };
849
+ var JSON_RE = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(;.+)?$/i;
850
+ function detectResponseType(request) {
851
+ const _contentType = request.headers.get("content-type");
852
+ const textTypes = /* @__PURE__ */ new Set([
853
+ "image/svg",
854
+ "application/xml",
855
+ "application/xhtml",
856
+ "application/html"
857
+ ]);
858
+ if (!_contentType) return "json";
859
+ const contentType = _contentType.split(";").shift() || "";
860
+ if (JSON_RE.test(contentType)) return "json";
861
+ if (textTypes.has(contentType) || contentType.startsWith("text/")) return "text";
862
+ return "blob";
863
+ }
864
+ function isJSONParsable(value) {
865
+ try {
866
+ JSON.parse(value);
867
+ return true;
868
+ } catch (error) {
869
+ return false;
870
+ }
871
+ }
872
+ function isJSONSerializable(value) {
873
+ if (value === void 0) return false;
874
+ const t = typeof value;
875
+ if (t === "string" || t === "number" || t === "boolean" || t === null) return true;
876
+ if (t !== "object") return false;
877
+ if (Array.isArray(value)) return true;
878
+ if (value.buffer) return false;
879
+ return value.constructor && value.constructor.name === "Object" || typeof value.toJSON === "function";
880
+ }
881
+ function jsonParse(text) {
882
+ try {
883
+ return JSON.parse(text);
884
+ } catch (error) {
885
+ return text;
886
+ }
887
+ }
888
+ function isFunction(value) {
889
+ return typeof value === "function";
890
+ }
891
+ function getFetch(options) {
892
+ if (options == null ? void 0 : options.customFetchImpl) return options.customFetchImpl;
893
+ if (typeof globalThis !== "undefined" && isFunction(globalThis.fetch)) return globalThis.fetch;
894
+ if (typeof window !== "undefined" && isFunction(window.fetch)) return window.fetch;
895
+ throw new Error("No fetch implementation found");
896
+ }
897
+ function mergeHeaders(...sources) {
898
+ const merged = {};
899
+ for (const source of sources) {
900
+ if (!source) continue;
901
+ if (source instanceof Headers) source.forEach((value, key) => {
902
+ merged[key] = value;
903
+ });
904
+ else {
905
+ const entries = Array.isArray(source) ? source : Object.entries(source);
906
+ for (const [key, value] of entries) if (value !== null && value !== void 0) merged[key] = value;
907
+ }
908
+ }
909
+ return merged;
910
+ }
911
+ async function getHeaders(opts) {
912
+ const headers = new Headers(mergeHeaders(opts == null ? void 0 : opts.headers, await getAuthHeader(opts)));
913
+ if (!headers.has("content-type")) {
914
+ const contentType = detectContentType(opts == null ? void 0 : opts.body);
915
+ if (contentType) headers.set("content-type", contentType);
916
+ }
917
+ return headers;
918
+ }
919
+ function detectContentType(body) {
920
+ if (isJSONSerializable(body)) return "application/json";
921
+ return null;
922
+ }
923
+ function getMediaType(headers) {
924
+ const contentType = headers.get("content-type");
925
+ return contentType ? contentType.split(";")[0].trim().toLowerCase() : null;
926
+ }
927
+ function getBody(options, headers) {
928
+ const { body } = options;
929
+ if (!body) return null;
930
+ if (!isJSONSerializable(body)) return body;
931
+ if (typeof body === "string") return body;
932
+ if (getMediaType(headers) === "application/x-www-form-urlencoded") return new URLSearchParams(body).toString();
933
+ return JSON.stringify(body);
934
+ }
935
+ function getMethod(url, options) {
936
+ var _a;
937
+ if (options == null ? void 0 : options.method) return options.method.toUpperCase();
938
+ if (url.startsWith("@")) {
939
+ const pMethod = (_a = url.split("@")[1]) == null ? void 0 : _a.split("/")[0];
940
+ if (!methods.includes(pMethod)) return (options == null ? void 0 : options.body) ? "POST" : "GET";
941
+ return pMethod.toUpperCase();
942
+ }
943
+ return (options == null ? void 0 : options.body) ? "POST" : "GET";
944
+ }
945
+ function getTimeout(options, controller) {
946
+ let abortTimeout;
947
+ if (!(options == null ? void 0 : options.signal) && (options == null ? void 0 : options.timeout)) abortTimeout = setTimeout(() => controller == null ? void 0 : controller.abort(), options == null ? void 0 : options.timeout);
948
+ return {
949
+ abortTimeout,
950
+ clearTimeout: () => {
951
+ if (abortTimeout) clearTimeout(abortTimeout);
952
+ }
953
+ };
954
+ }
955
+ var ValidationError = class _ValidationError extends Error {
956
+ constructor(issues, message) {
957
+ super(message || JSON.stringify(issues, null, 2));
958
+ this.issues = issues;
959
+ Object.setPrototypeOf(this, _ValidationError.prototype);
960
+ }
961
+ };
962
+ async function parseStandardSchema(schema, input) {
963
+ const result = await schema["~standard"].validate(input);
964
+ if (result.issues) throw new ValidationError(result.issues);
965
+ return result.value;
966
+ }
967
+ var methods = [
968
+ "get",
969
+ "post",
970
+ "put",
971
+ "patch",
972
+ "delete"
973
+ ];
974
+ var applySchemaPlugin = (config) => ({
975
+ id: "apply-schema",
976
+ name: "Apply Schema",
977
+ version: "1.0.0",
978
+ async init(url, options) {
979
+ var _a, _b, _c, _d;
980
+ const schema = ((_b = (_a = config.plugins) == null ? void 0 : _a.find((plugin) => {
981
+ var _a2;
982
+ return ((_a2 = plugin.schema) == null ? void 0 : _a2.config) ? url.startsWith(plugin.schema.config.baseURL || "") || url.startsWith(plugin.schema.config.prefix || "") : false;
983
+ })) == null ? void 0 : _b.schema) || config.schema;
984
+ if (schema) {
985
+ let urlKey = url;
986
+ if ((_c = schema.config) == null ? void 0 : _c.prefix) {
987
+ if (urlKey.startsWith(schema.config.prefix)) {
988
+ urlKey = urlKey.replace(schema.config.prefix, "");
989
+ if (schema.config.baseURL) url = url.replace(schema.config.prefix, schema.config.baseURL);
990
+ }
991
+ }
992
+ if ((_d = schema.config) == null ? void 0 : _d.baseURL) {
993
+ if (urlKey.startsWith(schema.config.baseURL)) urlKey = urlKey.replace(schema.config.baseURL, "");
994
+ }
995
+ if (urlKey.startsWith("/") && urlKey.charAt(1) === "@") urlKey = urlKey.substring(1);
996
+ const keySchema = schema.schema[urlKey];
997
+ if (keySchema) {
998
+ let validatedHeaders = options == null ? void 0 : options.headers;
999
+ if (keySchema.headers && !(options == null ? void 0 : options.disableValidation)) {
1000
+ const normalizedHeaders = {};
1001
+ if (options == null ? void 0 : options.headers) {
1002
+ if (options.headers instanceof Headers) options.headers.forEach((value, key) => {
1003
+ normalizedHeaders[key.toLowerCase()] = value;
1004
+ });
1005
+ else if (typeof options.headers === "object") {
1006
+ for (const [key, value] of Object.entries(options.headers)) if (value !== null && value !== void 0) normalizedHeaders[key.toLowerCase()] = value;
1007
+ }
1008
+ }
1009
+ const validated = await parseStandardSchema(keySchema.headers, normalizedHeaders);
1010
+ const finalHeaders = {};
1011
+ for (const [key, value] of Object.entries(validated)) finalHeaders[key.toLowerCase()] = value;
1012
+ validatedHeaders = finalHeaders;
1013
+ }
1014
+ let opts = __spreadProps(__spreadValues({}, options), {
1015
+ method: keySchema.method,
1016
+ output: keySchema.output,
1017
+ headers: validatedHeaders
1018
+ });
1019
+ if (!(options == null ? void 0 : options.disableValidation)) opts = __spreadProps(__spreadValues({}, opts), {
1020
+ body: keySchema.input ? await parseStandardSchema(keySchema.input, options == null ? void 0 : options.body) : options == null ? void 0 : options.body,
1021
+ params: keySchema.params ? await parseStandardSchema(keySchema.params, options == null ? void 0 : options.params) : options == null ? void 0 : options.params,
1022
+ query: keySchema.query ? await parseStandardSchema(keySchema.query, options == null ? void 0 : options.query) : options == null ? void 0 : options.query
1023
+ });
1024
+ return {
1025
+ url,
1026
+ options: opts
1027
+ };
1028
+ }
1029
+ }
1030
+ return {
1031
+ url,
1032
+ options
1033
+ };
1034
+ }
1035
+ });
1036
+ var createFetch = (config) => {
1037
+ async function $fetch(url, options) {
1038
+ const opts = __spreadProps(__spreadValues(__spreadValues({}, config), options), {
1039
+ headers: mergeHeaders(config == null ? void 0 : config.headers, options == null ? void 0 : options.headers),
1040
+ plugins: [
1041
+ ...(config == null ? void 0 : config.plugins) || [],
1042
+ applySchemaPlugin(config || {}),
1043
+ ...(options == null ? void 0 : options.plugins) || []
1044
+ ]
1045
+ });
1046
+ if (config == null ? void 0 : config.catchAllError) try {
1047
+ return await betterFetch(url, opts);
1048
+ } catch (error) {
1049
+ return {
1050
+ data: null,
1051
+ error: {
1052
+ status: 500,
1053
+ statusText: "Fetch Error",
1054
+ message: "Fetch related error. Captured by catchAllError option. See error property for more details.",
1055
+ error
1056
+ }
1057
+ };
1058
+ }
1059
+ return await betterFetch(url, opts);
1060
+ }
1061
+ return $fetch;
1062
+ };
1063
+ var isReservedPathSegment = (value) => value === "." || value === "..";
1064
+ function encodePathSegment(segment, pathParams) {
1065
+ let pathSegment = segment;
1066
+ for (const [key, value] of pathParams) pathSegment = pathSegment.replace(key, value);
1067
+ if (isReservedPathSegment(pathSegment)) throw new TypeError("Path parameters cannot be reserved path segments");
1068
+ return encodeURIComponent(pathSegment);
1069
+ }
1070
+ function getURL2(url, option) {
1071
+ const { baseURL, params, query } = option || {
1072
+ query: {},
1073
+ params: {},
1074
+ baseURL: ""
1075
+ };
1076
+ let basePath = url.startsWith("http") ? url.split("/").slice(0, 3).join("/") : baseURL || "";
1077
+ if (url.startsWith("@")) {
1078
+ const m = url.toString().split("@")[1].split("/")[0];
1079
+ if (methods.includes(m)) url = url.replace(`@${m}/`, "/");
1080
+ }
1081
+ if (!basePath.endsWith("/")) basePath += "/";
1082
+ let [path, urlQuery] = url.replace(basePath, "").split("?");
1083
+ const queryParams = new URLSearchParams(urlQuery);
1084
+ for (const [key, value] of Object.entries(query || {})) {
1085
+ if (value == null) continue;
1086
+ let serializedValue;
1087
+ if (typeof value === "string") serializedValue = value;
1088
+ else if (Array.isArray(value)) {
1089
+ for (const val of value) queryParams.append(key, val);
1090
+ continue;
1091
+ } else serializedValue = JSON.stringify(value);
1092
+ queryParams.set(key, serializedValue);
1093
+ }
1094
+ const pathParams = /* @__PURE__ */ new Map();
1095
+ if (params) if (Array.isArray(params)) {
1096
+ const paramPaths = path.split("/").filter((p) => p.startsWith(":"));
1097
+ for (const [index, key] of paramPaths.entries()) {
1098
+ const value = params[index];
1099
+ pathParams.set(key, String(value));
1100
+ }
1101
+ } else for (const [key, value] of Object.entries(params)) pathParams.set(`:${key}`, String(value));
1102
+ path = path.split("/").map((segment) => encodePathSegment(segment, pathParams)).join("/");
1103
+ path = path.replace(/^\/+/, "");
1104
+ let queryParamString = queryParams.toString();
1105
+ queryParamString = queryParamString.length > 0 ? `?${queryParamString}`.replace(/\+/g, "%20") : "";
1106
+ if (!basePath.startsWith("http")) return `${basePath}${path}${queryParamString}`;
1107
+ return new URL(`${path}${queryParamString}`, basePath);
1108
+ }
1109
+ var betterFetch = async (url, options) => {
1110
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1111
+ const { hooks, url: __url, options: opts } = await initializePlugins(url, options);
1112
+ const fetch = getFetch(opts);
1113
+ const controller = new AbortController();
1114
+ const signal = (_a = opts.signal) != null ? _a : controller.signal;
1115
+ const _url = getURL2(__url, opts);
1116
+ const headers = await getHeaders(opts);
1117
+ const body = getBody(opts, headers);
1118
+ const method = getMethod(__url, opts);
1119
+ const context = __spreadProps(__spreadValues({}, opts), {
1120
+ url: _url,
1121
+ headers,
1122
+ body,
1123
+ method,
1124
+ signal
1125
+ });
1126
+ for (const onRequest of hooks.onRequest) if (onRequest) {
1127
+ const res = await onRequest(context);
1128
+ if (typeof res === "object" && res !== null) Object.assign(context, res);
1129
+ }
1130
+ if ("pipeTo" in context && typeof context.pipeTo === "function" || typeof ((_b = options == null ? void 0 : options.body) == null ? void 0 : _b.pipe) === "function") {
1131
+ if (!("duplex" in context)) context.duplex = "half";
1132
+ }
1133
+ const { clearTimeout: clearTimeout2 } = getTimeout(opts, controller);
1134
+ let response = await fetch(context.url, context);
1135
+ clearTimeout2();
1136
+ const responseContext = {
1137
+ response,
1138
+ request: context
1139
+ };
1140
+ for (const onResponse of hooks.onResponse) if (onResponse) {
1141
+ const r = await onResponse(__spreadProps(__spreadValues({}, responseContext), { response: ((_c = options == null ? void 0 : options.hookOptions) == null ? void 0 : _c.cloneResponse) ? response.clone() : response }));
1142
+ if (r instanceof Response) response = r;
1143
+ else if (typeof r === "object" && r !== null) response = r.response;
1144
+ }
1145
+ if (response.ok) {
1146
+ if (!(context.method !== "HEAD")) return {
1147
+ data: "",
1148
+ error: null
1149
+ };
1150
+ const responseType = detectResponseType(response);
1151
+ const successContext = {
1152
+ data: null,
1153
+ response,
1154
+ request: context
1155
+ };
1156
+ if (responseType === "json" || responseType === "text") {
1157
+ const text = await response.text();
1158
+ successContext.data = await ((_d = context.jsonParser) != null ? _d : jsonParse)(text);
1159
+ } else successContext.data = await response[responseType]();
1160
+ if (context == null ? void 0 : context.output) {
1161
+ if (context.output && !context.disableValidation) successContext.data = await parseStandardSchema(context.output, successContext.data);
1162
+ }
1163
+ for (const onSuccess of hooks.onSuccess) if (onSuccess) await onSuccess(__spreadProps(__spreadValues({}, successContext), { response: ((_e = options == null ? void 0 : options.hookOptions) == null ? void 0 : _e.cloneResponse) ? response.clone() : response }));
1164
+ if (options == null ? void 0 : options.throw) return successContext.data;
1165
+ return {
1166
+ data: successContext.data,
1167
+ error: null
1168
+ };
1169
+ }
1170
+ const parser = (_f = options == null ? void 0 : options.jsonParser) != null ? _f : jsonParse;
1171
+ const responseText = await response.text();
1172
+ const isJSONResponse = isJSONParsable(responseText);
1173
+ const errorObject = isJSONResponse ? await parser(responseText) : null;
1174
+ const errorContext = {
1175
+ response,
1176
+ responseText,
1177
+ request: context,
1178
+ error: __spreadProps(__spreadValues({}, errorObject), {
1179
+ status: response.status,
1180
+ statusText: response.statusText
1181
+ })
1182
+ };
1183
+ for (const onError of hooks.onError) if (onError) await onError(__spreadProps(__spreadValues({}, errorContext), { response: ((_g = options == null ? void 0 : options.hookOptions) == null ? void 0 : _g.cloneResponse) ? response.clone() : response }));
1184
+ if (options == null ? void 0 : options.retry) {
1185
+ const retryStrategy = createRetryStrategy(options.retry);
1186
+ const _retryAttempt = (_h = options.retryAttempt) != null ? _h : 0;
1187
+ if (await retryStrategy.shouldAttemptRetry(_retryAttempt, response)) {
1188
+ for (const onRetry of hooks.onRetry) if (onRetry) await onRetry(responseContext);
1189
+ const delay = retryStrategy.getDelay(_retryAttempt);
1190
+ await new Promise((resolve) => setTimeout(resolve, delay));
1191
+ return await betterFetch(url, __spreadProps(__spreadValues({}, options), { retryAttempt: _retryAttempt + 1 }));
1192
+ }
1193
+ }
1194
+ if (options == null ? void 0 : options.throw) throw new BetterFetchError(response.status, response.statusText, isJSONResponse ? errorObject : responseText);
1195
+ return {
1196
+ data: null,
1197
+ error: __spreadProps(__spreadValues({}, errorObject), {
1198
+ status: response.status,
1199
+ statusText: response.statusText
1200
+ })
1201
+ };
1202
+ };
1203
+ /**
1204
+ * Pievo server data directory — server-side file state, and (for the embedded
1205
+ * pglite tier) the on-disk Postgres data dir at `<dataDir>/pgdata`. Locally it
1206
+ * defaults to `~/.pievo`. Override with `PIEVO_DATA_DIR`. A hosted-Postgres tier
1207
+ * still uses `<dataDir>/blobs` unless R2 is configured.
1208
+ */
1209
+ function dataDir() {
1210
+ return process.env.PIEVO_DATA_DIR?.trim() || path.join(os.homedir(), ".pievo");
1211
+ }
1212
+ /**
1213
+ * Supabase (or any Postgres) connection string for the RUNTIME app process. When
1214
+ * set, the server uses the postgres-js driver (hosted prod/staging). Pool options
1215
+ * are MODE-AWARE (`db/poolOptions.ts`): a Supabase TRANSACTION pooler URL
1216
+ * (`:6543`) gets `prepare:false`; a SESSION pooler / direct URL (`:5432`) gets
1217
+ * `prepare:true` (backend pinned per connection — safe and faster). Either form
1218
+ * is supported; `dbPoolMode()` below overrides the port-based detection.
1219
+ * When UNSET, the server falls back to the embedded, file-backed pglite database
1220
+ * at `<dataDir>/pgdata` (local dev + light self-host + tests) — zero external DB.
1221
+ */
1222
+ function databaseUrl() {
1223
+ return process.env.DATABASE_URL?.trim() || void 0;
1224
+ }
1225
+ /**
1226
+ * Explicit pool-mode override for the runtime postgres-js pool
1227
+ * (`PIEVO_DB_POOL_MODE=transaction|session`). Normally UNSET — the mode is
1228
+ * detected from `DATABASE_URL`'s port (`:6543` ⇒ transaction pooler; see
1229
+ * `db/poolOptions.ts` `isTransactionPooler`). This is the escape hatch for a
1230
+ * NONSTANDARD-PORT or self-hosted pooler the port heuristic can't classify.
1231
+ * Getting it wrong on a real transaction pooler (`session` when the pooler is
1232
+ * transaction-mode) re-arms the prepared-statement breakage the detection
1233
+ * exists to prevent — set it only when you know the pooler's mode.
1234
+ */
1235
+ function dbPoolMode() {
1236
+ const raw = process.env.PIEVO_DB_POOL_MODE?.trim().toLowerCase();
1237
+ if (raw === "transaction" || raw === "session") return raw;
1238
+ }
1239
+ function r2Config() {
1240
+ const accountId = process.env.PIEVO_R2_ACCOUNT_ID?.trim();
1241
+ const bucket = process.env.PIEVO_R2_BUCKET?.trim();
1242
+ const accessKeyId = process.env.PIEVO_R2_ACCESS_KEY_ID?.trim();
1243
+ const secretAccessKey = process.env.PIEVO_R2_SECRET_ACCESS_KEY?.trim();
1244
+ const explicitEndpoint = process.env.PIEVO_R2_ENDPOINT?.trim();
1245
+ const region = process.env.PIEVO_R2_REGION?.trim();
1246
+ const endpoint = explicitEndpoint || (accountId ? `https://${accountId}.r2.cloudflarestorage.com` : void 0);
1247
+ if (!!!(accountId || bucket || accessKeyId || secretAccessKey || explicitEndpoint || region)) return null;
1248
+ const missing = [
1249
+ !bucket && "PIEVO_R2_BUCKET",
1250
+ !accessKeyId && "PIEVO_R2_ACCESS_KEY_ID",
1251
+ !secretAccessKey && "PIEVO_R2_SECRET_ACCESS_KEY",
1252
+ !endpoint && "PIEVO_R2_ACCOUNT_ID or PIEVO_R2_ENDPOINT"
1253
+ ].filter(Boolean);
1254
+ if (missing.length) throw new Error(`incomplete R2 configuration — missing ${missing.join(", ")}`);
1255
+ return {
1256
+ bucket,
1257
+ endpoint,
1258
+ accessKeyId,
1259
+ secretAccessKey,
1260
+ region: region || "auto"
1261
+ };
1262
+ }
1263
+ /**
1264
+ * Artifact-storage retention / GC knobs (see gateway/retention.ts). Read lazily
1265
+ * from env at call time (not at module load) so tests can set them per-case and
1266
+ * a deploy can tune them without a rebuild. Each has a safe, generous default —
1267
+ * the bias is "keep storage bounded without ever surprising a healthy loop".
1268
+ */
1269
+ function posIntEnv(name, fallback) {
1270
+ const raw = process.env[name]?.trim();
1271
+ if (!raw) return fallback;
1272
+ const n = Number(raw);
1273
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
1274
+ }
1275
+ /**
1276
+ * How many of the most recent run snapshots to keep per loop. Older ones are
1277
+ * pruned (their now-unreferenced blobs then become GC-collectable). The per-run
1278
+ * diff only needs the immediately-prior snapshot, so 20 keeps ample history for
1279
+ * the "Changes" view while bounding the table + blob retention. Default 20.
1280
+ */
1281
+ function snapshotRetention() {
1282
+ return posIntEnv("PIEVO_SNAPSHOT_RETENTION", 20);
1283
+ }
1284
+ /**
1285
+ * GC grace window: a blob whose metadata row is younger than this is NEVER
1286
+ * collected, even if currently unreferenced. This is the concurrency guard —
1287
+ * a blob a sync just wrote (and is about to reference) can't be swept out from
1288
+ * under it. Generous default (1h) since a leaked blob is only a cost bug that
1289
+ * the next GC pass reclaims anyway. Default 3,600,000ms (1 hour).
1290
+ */
1291
+ function blobGcGraceMs() {
1292
+ return posIntEnv("PIEVO_BLOB_GC_GRACE_MS", 3600 * 1e3);
1293
+ }
1294
+ /**
1295
+ * How often the periodic storage-maintenance pass runs (prune snapshots → GC
1296
+ * blobs). Default 15 minutes. Kept independent of the faster offline-sweep tick.
1297
+ */
1298
+ function gcIntervalMs() {
1299
+ return posIntEnv("PIEVO_GC_INTERVAL_MS", 900 * 1e3);
1300
+ }
1301
+ /**
1302
+ * DB watchdog (`server/dbWatchdog.ts`) — the ACTUAL auto-recovery backstop for a
1303
+ * wedged postgres-js pool. Fly's `[http_service.checks]` failing only pulls the
1304
+ * machine from load balancing; it never restarts the VM (the 2026-07-12 outage:
1305
+ * the pool wedged, `/api/health/db` hung, the check went critical, and the box
1306
+ * stayed down ~9h with no auto-restart). The watchdog turns a HANG into a process
1307
+ * EXIT so Fly's `restart.policy = "on-failure"` kicks in with a fresh pool.
1308
+ *
1309
+ * Enabled by default ONLY on the hosted postgres tier (a wedged transaction-pooler
1310
+ * connection is the failure mode); OFF for the embedded pglite tier and under
1311
+ * vitest (a test must never `process.exit`). Force either way with
1312
+ * `PIEVO_DB_WATCHDOG=on|off`.
1313
+ */
1314
+ function dbWatchdogEnabled() {
1315
+ const raw = process.env.PIEVO_DB_WATCHDOG?.trim().toLowerCase();
1316
+ if (raw === "on") return true;
1317
+ if (raw === "off") return false;
1318
+ if (process.env.VITEST || false) return false;
1319
+ return Boolean(databaseUrl());
1320
+ }
1321
+ function dbWatchdogIntervalMs() {
1322
+ return posIntEnv("PIEVO_DB_WATCHDOG_INTERVAL_MS", 2e4);
1323
+ }
1324
+ function dbWatchdogTimeoutMs() {
1325
+ return posIntEnv("PIEVO_DB_WATCHDOG_TIMEOUT_MS", 5e3);
1326
+ }
1327
+ /**
1328
+ * Consecutive failed/timed-out pings before the watchdog exits the process. A
1329
+ * healthy ping resets the counter, so this only trips on a SUSTAINED wedge — a
1330
+ * brief blip never restarts the box. Default 3 (≈60-75s to detect at 20s cadence).
1331
+ */
1332
+ function dbWatchdogFailureThreshold() {
1333
+ return posIntEnv("PIEVO_DB_WATCHDOG_FAILURES", 3);
1334
+ }
1335
+ /**
1336
+ * Uses pino-pretty as a *synchronous* destination stream (not a worker-thread
1337
+ * transport) so logs flush before process.exit() in the one-shot CLIs. Writes to
1338
+ * stderr (fd 2) to keep stdout clean for QR codes and human CLI output.
1339
+ */
1340
+ var level = (process.env.PIEVO_LOG_LEVEL || "info").toLowerCase();
1341
+ var stream = (0, import_pino_pretty.default)({
1342
+ destination: 2,
1343
+ colorize: true,
1344
+ translateTime: "SYS:HH:MM:ss",
1345
+ ignore: "pid,hostname"
1346
+ });
1347
+ var logger = (0, import_pino.default)({ level }, stream);
1348
+ /**
1349
+ * Pure pool-option derivation for the hosted postgres-js tier. LEAF module by
1350
+ * design: no env reads, no DB opens — `db/index.ts` supplies the URL (and the
1351
+ * optional `PIEVO_DB_POOL_MODE` override via `env.ts`), tests import this
1352
+ * directly without booting a database.
1353
+ */
1354
+ /**
1355
+ * Whether `url` points at a pgBouncer/Supavisor TRANSACTION pooler. Detection
1356
+ * is by the conventional port `:6543` (Supabase's transaction-mode port).
1357
+ *
1358
+ * Why it matters: postgres-js pipelines queries and caches prepared statements
1359
+ * per connection, but a transaction pooler reassigns the backend connection per
1360
+ * transaction, which both breaks cached prepared statements and is implicated
1361
+ * in the pool-wedge failure mode (porsager/postgres#970). Session mode (:5432)
1362
+ * pins one backend per client connection, so neither problem exists there.
1363
+ *
1364
+ * LIMITATION: a nonstandard-port or self-hosted transaction pooler will NOT be
1365
+ * detected here — set `PIEVO_DB_POOL_MODE=transaction` (see `env.ts`
1366
+ * `dbPoolMode`) to override. Getting that wrong on a real transaction pooler
1367
+ * re-arms the prepared-statement breakage.
1368
+ */
1369
+ function isTransactionPooler(url) {
1370
+ try {
1371
+ return new URL(url).port === "6543";
1372
+ } catch {
1373
+ return false;
1374
+ }
1375
+ }
1376
+ /**
1377
+ * Mode-aware pool options:
1378
+ *
1379
+ * - TRANSACTION pooler (:6543): `prepare:false` is mandatory — the backend is
1380
+ * reassigned per transaction, so cached prepared statements break.
1381
+ * - SESSION pooler / direct (:5432): the backend is pinned per connection, so
1382
+ * `prepare:true` is safe AND faster (cached plans), and the session-scoped
1383
+ * `statement_timeout` is actually enforced on the executing backend.
1384
+ *
1385
+ * The shared knobs are the conservative self-healing ring for one always-on
1386
+ * machine (see `db/index.ts` + api.health.db.ts for the incident history).
1387
+ * Pass `transactionPooler` explicitly to override the port-based detection
1388
+ * (the `PIEVO_DB_POOL_MODE` escape hatch).
1389
+ */
1390
+ function poolOptionsFor(url, transactionPooler = isTransactionPooler(url)) {
1391
+ return {
1392
+ prepare: !transactionPooler,
1393
+ max: 10,
1394
+ idle_timeout: 30,
1395
+ connect_timeout: 15,
1396
+ max_lifetime: 1800,
1397
+ connection: { statement_timeout: 3e4 }
1398
+ };
1399
+ }
1400
+ /** The coding agent a loop is bound to AND executed with (BYOA on the owner's
1401
+ * machine): `claude-code` → Claude Code, `codex` → `codex exec`, `pi` → Pi.
1402
+ * Non-Claude agents may still have thinner daemon telemetry until stream
1403
+ * integration lands; execution itself is real for every value.
1404
+ *
1405
+ * Runtime SINGLE SOURCE (anti-drift): every server consumer DERIVES from this
1406
+ * array — the `CodingAgent` type here, the `db/schema.ts` `CodingAgent` type AND
1407
+ * the `loops.agent` column enum, the edit validator (`coerceCodingAgent`), and the
1408
+ * web agent `<select>` (LoopForm). So widening the set is a one-line edit HERE with
1409
+ * no other server change (the daemon's own enum in `packages/daemon/src/create.ts`
1410
+ * is a separate package, widened alongside). */
1411
+ var CODING_AGENTS = [
1412
+ "claude-code",
1413
+ "codex",
1414
+ "pi"
1415
+ ];
1416
+ function coerceCodingAgent(value) {
1417
+ return typeof value === "string" && CODING_AGENTS.includes(value) ? value : null;
1418
+ }
1419
+ var RESERVED_LOOP_TAGS = [
1420
+ "all loops",
1421
+ "active",
1422
+ "paused",
1423
+ "blocked"
1424
+ ];
1425
+ /**
1426
+ * Pievo business schema (Drizzle, Postgres `pg-core` dialect).
1427
+ *
1428
+ * Core machine / loop / run tables live alongside Better Auth ownership data.
1429
+ * `userId` is the owning user's id. We use
1430
+ * Drizzle (not raw SQL) so the store is single-sourced across the driver tiers
1431
+ * (postgres-js on Supabase; embedded pglite for local/self-host + tests) — the
1432
+ * query-builder API is identical, only `db/index.ts` branches the driver.
1433
+ *
1434
+ * Timestamp values are represented as ISO strings across both drivers. JSON
1435
+ * columns use `jsonb().$type<>()`; booleans use native `boolean()`.
1436
+ */
1437
+ var machines = pgTable("machines", {
1438
+ id: text("id").primaryKey(),
1439
+ userId: text("user_id").notNull(),
1440
+ name: text("name").notNull(),
1441
+ hostname: text("hostname"),
1442
+ platform: text("platform"),
1443
+ arch: text("arch"),
1444
+ daemonVersion: text("daemon_version"),
1445
+ /** Breaking machine protocol last observed on poll. */
1446
+ daemonProtocol: integer("daemon_protocol"),
1447
+ tokenHash: text("token_hash").notNull(),
1448
+ /**
1449
+ * Plaintext device token. Stored so the UI can re-show the connect command
1450
+ * anytime (MVP convenience — deviates from "store only the hash"; acceptable
1451
+ * for a self-hosted control plane where the DB is already the trust root).
1452
+ */
1453
+ token: text("token"),
1454
+ roots: jsonb("roots").$type(),
1455
+ lastSeen: text("last_seen"),
1456
+ online: boolean("online").notNull().default(false),
1457
+ createdAt: text("created_at").notNull()
1458
+ }, (t) => [index("machines_user_idx").on(t.userId)]);
1459
+ var loops = pgTable("loops", {
1460
+ id: text("id").primaryKey(),
1461
+ userId: text("user_id").notNull(),
1462
+ /** Execution machine (set at creation; no cross-machine fallback). */
1463
+ machineId: text("machine_id").notNull(),
1464
+ name: text("name").notNull(),
1465
+ tags: text("tags").array().notNull().default(sql`ARRAY[]::text[]`),
1466
+ prompt: text("prompt").notNull(),
1467
+ statusKeep: text("status_keep").notNull(),
1468
+ statusNoChange: text("status_no_change").notNull(),
1469
+ statusBlock: text("status_block").notNull(),
1470
+ /** Exact workdir-relative artifact paths collected by the daemon after provider exit. */
1471
+ artifacts: jsonb("artifacts").$type().notNull().default([]),
1472
+ cron: text("cron").notNull(),
1473
+ scheduleMode: text("schedule_mode", { enum: ["cron", "continuous"] }).notNull(),
1474
+ /** Cron-only overlap behavior. Continuous runs never overlap. */
1475
+ cronOverlap: text("cron_overlap", { enum: ["skip", "queue-one"] }).notNull(),
1476
+ continuousDelayMinutes: integer("continuous_delay_minutes").notNull(),
1477
+ timezone: text("timezone"),
1478
+ workdir: text("workdir").notNull(),
1479
+ /** Durable Stop-before-delete marker. A deleting loop is never claimable. */
1480
+ deleteRequestedAt: timestamp("delete_requested_at", {
1481
+ withTimezone: true,
1482
+ mode: "string"
1483
+ }),
1484
+ pauseCause: jsonb("pause_cause").$type(),
1485
+ model: text("model"),
1486
+ reasoningEffort: text("reasoning_effort"),
1487
+ /** Coding agent this loop is BOUND TO and EXECUTED with: the daemon's
1488
+ * `buildAgentSpawn` branches on this value (`claude-code` → claude,
1489
+ * `codex` → `codex exec`, `pi` → Pi print mode). Measured from the creating CLI's env when
1490
+ * detectable, else the declared/selected value. The text column carries
1491
+ * both a TypeScript enum and a database CHECK constraint. */
1492
+ agent: text("agent", { enum: CODING_AGENTS }).notNull(),
1493
+ enabled: boolean("enabled").notNull().default(true),
1494
+ nextRunAt: text("next_run_at"),
1495
+ /** The next recurring cadence occurrence not yet materialized as a run. Cron
1496
+ * advances this fact after each due occurrence; continuous clears it on due
1497
+ * and restores it from a successful/error terminal. */
1498
+ nextCadenceAt: text("next_cadence_at"),
1499
+ lastRunIndex: integer("last_run_index").notNull().default(0),
1500
+ createdAt: text("created_at").notNull(),
1501
+ updatedAt: text("updated_at").notNull()
1502
+ }, (t) => [
1503
+ index("loops_user_idx").on(t.userId),
1504
+ index("loops_machine_idx").on(t.machineId),
1505
+ index("delete_requested_loops").on(t.deleteRequestedAt).where(sql`${t.deleteRequestedAt} IS NOT NULL`),
1506
+ check("loops_agent_check", sql`${t.agent} IN ('claude-code', 'codex', 'pi')`),
1507
+ check("loops_tags_count_check", sql`cardinality(${t.tags}) <= 4`),
1508
+ check("loops_tags_reserved_check", sql`NOT (${t.tags} && ARRAY['all loops', 'active', 'paused', 'blocked']::text[])`),
1509
+ check("loops_schedule_mode_check", sql`${t.scheduleMode} IN ('cron', 'continuous')`),
1510
+ check("loops_cron_overlap_check", sql`${t.cronOverlap} IN ('skip', 'queue-one')`)
1511
+ ]);
1512
+ var runs = pgTable("runs", {
1513
+ id: text("id").primaryKey(),
1514
+ loopId: text("loop_id").notNull(),
1515
+ machineId: text("machine_id").notNull(),
1516
+ agent: text("agent", { enum: CODING_AGENTS }),
1517
+ model: text("model"),
1518
+ reasoningEffort: text("reasoning_effort"),
1519
+ runIndex: integer("run_index"),
1520
+ phase: text("phase", { enum: [
1521
+ "pending",
1522
+ "running",
1523
+ "done",
1524
+ "error",
1525
+ "canceled"
1526
+ ] }).notNull(),
1527
+ /** Durable queue authority. It only promotes system→owner; diagnostic trigger
1528
+ * reasons, if added later, must never drive lifecycle state. */
1529
+ requestedBy: text("requested_by", { enum: ["owner", "system"] }).notNull().default("system"),
1530
+ ts: text("ts").notNull(),
1531
+ createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP::text`),
1532
+ updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP::text`),
1533
+ status: text("status", { enum: [
1534
+ "keep",
1535
+ "no-change",
1536
+ "block"
1537
+ ] }),
1538
+ message: text("message"),
1539
+ durationMs: integer("duration_ms"),
1540
+ exitCode: integer("exit_code"),
1541
+ finalText: text("final_text"),
1542
+ error: text("error"),
1543
+ sessionId: text("session_id"),
1544
+ usage: jsonb("usage").$type(),
1545
+ /** Monotonic cancellation intent. Running becomes canceled only on daemon report. */
1546
+ cancelRequestedAt: timestamp("cancel_requested_at", {
1547
+ withTimezone: true,
1548
+ mode: "string"
1549
+ }),
1550
+ heartbeatAt: text("heartbeat_at"),
1551
+ reportIncident: jsonb("report_incident").$type()
1552
+ }, (t) => [
1553
+ index("runs_loop_idx").on(t.loopId),
1554
+ index("runs_phase_idx").on(t.phase),
1555
+ index("runs_machine_phase_ready_idx").on(t.machineId, t.phase),
1556
+ index("runs_loop_ts_idx").on(t.loopId, t.ts),
1557
+ uniqueIndex("runs_loop_run_index_idx").on(t.loopId, t.runIndex).where(sql`${t.runIndex} IS NOT NULL`),
1558
+ index("runs_loop_terminal_history_idx").on(t.loopId, t.runIndex).where(sql`${t.runIndex} IS NOT NULL AND ${t.phase} IN ('done', 'error', 'canceled')`),
1559
+ uniqueIndex("runs_loop_pending_idx").on(t.loopId).where(sql`${t.phase} = 'pending'`),
1560
+ uniqueIndex("one_running_run_per_loop").on(t.loopId).where(sql`${t.phase} = 'running'`),
1561
+ check("runs_agent_check", sql`${t.agent} IS NULL OR ${t.agent} IN ('claude-code', 'codex', 'pi')`),
1562
+ check("runs_phase_check", sql`${t.phase} IN ('pending', 'running', 'done', 'error', 'canceled')`),
1563
+ check("runs_requested_by_check", sql`${t.requestedBy} IN ('owner', 'system')`),
1564
+ check("runs_status_check", sql`${t.status} IS NULL OR ${t.status} IN ('keep', 'no-change', 'block')`)
1565
+ ]);
1566
+ var runLeases = pgTable("run_leases", {
1567
+ tokenHash: text("token_hash").primaryKey(),
1568
+ runId: text("run_id").notNull(),
1569
+ loopId: text("loop_id").notNull(),
1570
+ machineId: text("machine_id").notNull(),
1571
+ state: text("state", { enum: [
1572
+ "active",
1573
+ "terminal-grace",
1574
+ "reconciliation-only",
1575
+ "retired"
1576
+ ] }).notNull().default("active"),
1577
+ /** Null while active/retired; ISO only during terminal-grace. */
1578
+ expiresAt: text("expires_at"),
1579
+ createdAt: text("created_at").notNull()
1580
+ }, (t) => [
1581
+ index("run_leases_run_idx").on(t.runId),
1582
+ index("run_leases_loop_idx").on(t.loopId),
1583
+ check("run_leases_state_check", sql`${t.state} IN ('active', 'terminal-grace', 'reconciliation-only', 'retired')`)
1584
+ ]);
1585
+ var runReportReceipts = pgTable("run_report_receipts", {
1586
+ reportId: text("report_id").primaryKey(),
1587
+ runId: text("run_id").notNull(),
1588
+ payloadDigest: text("payload_digest").notNull(),
1589
+ ackStatus: integer("ack_status").notNull(),
1590
+ ackBody: jsonb("ack_body").$type().notNull(),
1591
+ createdAt: timestamp("created_at", {
1592
+ withTimezone: true,
1593
+ mode: "string"
1594
+ }).notNull()
1595
+ }, (t) => [index("report_receipts_created").on(t.createdAt)]);
1596
+ /** Durable evidence for a rejected terminal attempt. It is keyed by
1597
+ * sha256(reportId + canonical-payload-digest), is never loop-owned, and therefore
1598
+ * survives loop deletion just like normal report receipts. */
1599
+ var terminalReportIncidents = pgTable("terminal_report_incidents", {
1600
+ id: text("id").primaryKey(),
1601
+ runId: text("run_id").notNull(),
1602
+ reportId: text("report_id").notNull(),
1603
+ payloadDigest: text("payload_digest").notNull(),
1604
+ disposition: text("disposition", { enum: ["run-error", "telemetry-rejected"] }).$type().notNull(),
1605
+ ackBody: jsonb("ack_body").$type().notNull(),
1606
+ createdAt: timestamp("created_at", {
1607
+ withTimezone: true,
1608
+ mode: "string"
1609
+ }).notNull()
1610
+ }, (t) => [
1611
+ index("terminal_report_incidents_report_id").on(t.reportId),
1612
+ index("terminal_report_incidents_created").on(t.createdAt),
1613
+ check("terminal_report_incidents_disposition_check", sql`${t.disposition} IN ('run-error', 'telemetry-rejected')`)
1614
+ ]);
1615
+ var connectKeys = pgTable("connect_keys", {
1616
+ machineId: text("machine_id").primaryKey(),
1617
+ userId: text("user_id").notNull(),
1618
+ /** Mint time (ISO) — drives the TTL. */
1619
+ mintedAt: text("minted_at").notNull()
1620
+ });
1621
+ /**
1622
+ * One content-addressed blob (deduped across every loop/run). The bytes live in
1623
+ * the byte store under the hash; this row records verified byte presence and size.
1624
+ */
1625
+ var blobs = pgTable("blobs", {
1626
+ hash: text("hash").primaryKey(),
1627
+ size: integer("size").notNull(),
1628
+ createdAt: text("created_at").notNull()
1629
+ });
1630
+ /**
1631
+ * The current configured file set of each loop — one row per live or tombstoned
1632
+ * relative path. `hash` → `blobs.hash`; null when the file is deleted or oversize
1633
+ * (metadata-only, no bytes synced). The
1634
+ * unique (loopId, path) index is the upsert key the sync reconciliation drives.
1635
+ */
1636
+ var artifactFiles = pgTable("artifact_files", {
1637
+ id: text("id").primaryKey(),
1638
+ loopId: text("loop_id").notNull(),
1639
+ /** Normalized workdir-relative path (never absolute or escaping the workdir). */
1640
+ path: text("path").notNull(),
1641
+ hash: text("hash"),
1642
+ size: integer("size"),
1643
+ /** Bytes contain a NUL (set even for oversize metadata-only files). */
1644
+ binary: boolean("binary").notNull().default(false),
1645
+ /** File exceeds the per-file byte cap → metadata-only (path + size), no blob. */
1646
+ oversize: boolean("oversize").notNull().default(false),
1647
+ deleted: boolean("deleted").notNull().default(false),
1648
+ updatedAt: text("updated_at").notNull()
1649
+ }, (t) => [
1650
+ index("artifact_files_loop_idx").on(t.loopId),
1651
+ uniqueIndex("artifact_files_loop_path_idx").on(t.loopId, t.path),
1652
+ index("artifact_files_hash_idx").on(t.hash)
1653
+ ]);
1654
+ /**
1655
+ * The loop's full artifact manifest captured at each run's finalize — the input
1656
+ * to the per-run diff. Written cheaply on report (no diff computed on write);
1657
+ * `getRunDiff` lazily compares run N's snapshot with the preceding snapshot.
1658
+ * One row per run (runId primary key).
1659
+ */
1660
+ var runSnapshots = pgTable("run_snapshots", {
1661
+ runId: text("run_id").primaryKey(),
1662
+ loopId: text("loop_id").notNull(),
1663
+ manifest: jsonb("manifest").$type().notNull(),
1664
+ createdAt: text("created_at").notNull()
1665
+ }, (t) => [index("run_snapshots_loop_idx").on(t.loopId)]);
1666
+ var user = pgTable("user", {
1667
+ id: text("id").primaryKey(),
1668
+ name: text("name").notNull(),
1669
+ email: text("email").notNull().unique(),
1670
+ emailVerified: boolean("email_verified").default(false).notNull(),
1671
+ image: text("image"),
1672
+ createdAt: timestamp("created_at").defaultNow().notNull(),
1673
+ updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()).notNull()
1674
+ });
1675
+ var session = pgTable("session", {
1676
+ id: text("id").primaryKey(),
1677
+ expiresAt: timestamp("expires_at").notNull(),
1678
+ token: text("token").notNull().unique(),
1679
+ createdAt: timestamp("created_at").defaultNow().notNull(),
1680
+ updatedAt: timestamp("updated_at").$onUpdate(() => /* @__PURE__ */ new Date()).notNull(),
1681
+ ipAddress: text("ip_address"),
1682
+ userAgent: text("user_agent"),
1683
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" })
1684
+ }, (table) => [index("session_userId_idx").on(table.userId)]);
1685
+ var account = pgTable("account", {
1686
+ id: text("id").primaryKey(),
1687
+ accountId: text("account_id").notNull(),
1688
+ providerId: text("provider_id").notNull(),
1689
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
1690
+ accessToken: text("access_token"),
1691
+ refreshToken: text("refresh_token"),
1692
+ idToken: text("id_token"),
1693
+ accessTokenExpiresAt: timestamp("access_token_expires_at"),
1694
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
1695
+ scope: text("scope"),
1696
+ password: text("password"),
1697
+ createdAt: timestamp("created_at").defaultNow().notNull(),
1698
+ updatedAt: timestamp("updated_at").$onUpdate(() => /* @__PURE__ */ new Date()).notNull()
1699
+ }, (table) => [index("account_userId_idx").on(table.userId)]);
1700
+ var verification = pgTable("verification", {
1701
+ id: text("id").primaryKey(),
1702
+ identifier: text("identifier").notNull(),
1703
+ value: text("value").notNull(),
1704
+ expiresAt: timestamp("expires_at").notNull(),
1705
+ createdAt: timestamp("created_at").defaultNow().notNull(),
1706
+ updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()).notNull()
1707
+ }, (table) => [index("verification_identifier_idx").on(table.identifier)]);
1708
+ relations(user, ({ many }) => ({
1709
+ sessions: many(session),
1710
+ accounts: many(account)
1711
+ }));
1712
+ relations(session, ({ one }) => ({ user: one(user, {
1713
+ fields: [session.userId],
1714
+ references: [user.id]
1715
+ }) }));
1716
+ relations(account, ({ one }) => ({ user: one(user, {
1717
+ fields: [account.userId],
1718
+ references: [user.id]
1719
+ }) }));
1720
+ /**
1721
+ * Postgres connection + Drizzle handle. The driver is chosen at boot from env
1722
+ * from environment configuration:
1723
+ *
1724
+ * - `DATABASE_URL` SET → postgres-js against Supabase (hosted prod/staging).
1725
+ * Pool options are mode-aware (`db/poolOptions.ts`):
1726
+ * `prepare:false` on the `:6543` transaction pooler,
1727
+ * `prepare:true` on a `:5432` session/direct URL.
1728
+ * - `DATABASE_URL` UNSET → embedded pglite, file-backed at `<dataDir>/pgdata`
1729
+ * (local dev + light self-host + tests). Same pg
1730
+ * dialect, same migrations, zero external database.
1731
+ *
1732
+ * `store.ts` is single-sourced across both — the drizzle query-builder API is
1733
+ * identical; only this file branches. A `globalThis` guard keeps the handle a
1734
+ * true singleton across dev HMR reloads (the Scheduler shares it in-process).
1735
+ *
1736
+ * The fresh-deploy baseline lives in `packages/server/drizzle`. In
1737
+ * the HOSTED tier they're applied out-of-process via `pnpm db:migrate` before
1738
+ * `start` (over the DIRECT `:5432` URL); `runMigrations()` also applies them
1739
+ * in-process (the pglite tier's only path, plus dev boot + tests).
1740
+ */
1741
+ var schema = {
1742
+ machines,
1743
+ loops,
1744
+ runs,
1745
+ blobs,
1746
+ artifactFiles,
1747
+ runSnapshots,
1748
+ runLeases,
1749
+ runReportReceipts,
1750
+ terminalReportIncidents,
1751
+ connectKeys,
1752
+ user,
1753
+ session,
1754
+ account,
1755
+ verification
1756
+ };
1757
+ var g = globalThis;
1758
+ /** Host (host:port, NEVER credentials) of a Postgres URL, for boot logging. */
1759
+ function pgHost(url) {
1760
+ try {
1761
+ return new URL(url).host || "unknown";
1762
+ } catch {
1763
+ return "unparseable";
1764
+ }
1765
+ }
1766
+ function open() {
1767
+ if (g.__pievoDb && g.__pievoClient && g.__pievoDriver) return {
1768
+ db: g.__pievoDb,
1769
+ client: g.__pievoClient,
1770
+ driver: g.__pievoDriver
1771
+ };
1772
+ const url = databaseUrl();
1773
+ if (url) {
1774
+ logger.info({
1775
+ tier: "postgres",
1776
+ host: pgHost(url)
1777
+ }, "database tier: postgres");
1778
+ if (!/sslmode=/i.test(url)) logger.warn({ host: pgHost(url) }, "DATABASE_URL has no sslmode — the connection may be plaintext; append ?sslmode=require");
1779
+ const mode = dbPoolMode();
1780
+ const client = src_default(url, poolOptionsFor(url, mode ? mode === "transaction" : isTransactionPooler(url)));
1781
+ const db = drizzle(client, { schema });
1782
+ g.__pievoClient = client;
1783
+ g.__pievoDb = db;
1784
+ g.__pievoDriver = "postgres";
1785
+ return {
1786
+ db,
1787
+ client,
1788
+ driver: "postgres"
1789
+ };
1790
+ }
1791
+ if (process.env.PIEVO_DB !== "pglite") throw new Error("refusing to boot the ephemeral embedded database in production — set DATABASE_URL, or PIEVO_DB=pglite to opt in");
1792
+ const dir = path.join(dataDir(), "pgdata");
1793
+ logger.info({
1794
+ tier: "pglite",
1795
+ pgdata: dir
1796
+ }, "database tier: pglite");
1797
+ fs.mkdirSync(dataDir(), { recursive: true });
1798
+ const client = new Ve(dir);
1799
+ const db = drizzle$1(client, { schema });
1800
+ g.__pievoClient = client;
1801
+ g.__pievoDb = db;
1802
+ g.__pievoDriver = "pglite";
1803
+ return {
1804
+ db,
1805
+ client,
1806
+ driver: "pglite"
1807
+ };
1808
+ }
1809
+ var opened = open();
1810
+ var db = opened.db;
1811
+ opened.client;
1812
+ /**
1813
+ * Migrations dir. In DEV this file lives at `src/db`, so the folder is at the
1814
+ * package root (`../../drizzle`). In the BUILT server the module is bundled under
1815
+ * `.output/server/{_ssr,_libs}` and `copy-pglite-assets.mjs` lands the folder at
1816
+ * `.output/server/drizzle` (`../drizzle`). Return the first candidate that exists
1817
+ * so the embedded pglite tier migrates in-process from either layout.
1818
+ */
1819
+ function migrationsDir() {
1820
+ const here = path.dirname(fileURLToPath(import.meta.url));
1821
+ const devDir = path.resolve(here, "..", "..", "drizzle");
1822
+ const builtDir = path.resolve(here, "..", "drizzle");
1823
+ if (fs.existsSync(devDir)) return devDir;
1824
+ if (fs.existsSync(builtDir)) return builtDir;
1825
+ return devDir;
1826
+ }
1827
+ /**
1828
+ * Apply generated migrations IN-PROCESS (idempotent). This is the PGLITE tier's
1829
+ * only migration path (also dev boot + tests). The HOSTED postgres tier is a
1830
+ * NO-OP here: `prestart.mjs` (and `pnpm db:migrate`) own hosted migrations, run
1831
+ * once out-of-process over the DIRECT (:5432) URL before serving — re-migrating
1832
+ * over a second connection on every boot only risks a wedged connection hanging
1833
+ * boot and double-running the advisory lock.
1834
+ */
1835
+ async function runMigrations() {
1836
+ if (opened.driver === "postgres") return;
1837
+ const dir = migrationsDir();
1838
+ if (!fs.existsSync(dir)) return;
1839
+ const { migrate } = await import("../_libs/drizzle-orm+postgres.mjs").then((n) => n.M);
1840
+ await migrate(opened.db, { migrationsFolder: dir });
1841
+ }
1842
+ /**
1843
+ * Best-effort clean shutdown of the runtime database client (called from the
1844
+ * boot AbortController on SIGINT/SIGTERM). PGlite must be closed explicitly:
1845
+ * after migrations it owns a referenced WASM timer that otherwise keeps Node
1846
+ * alive until the managed launcher's graceful-stop deadline. Never throws —
1847
+ * shutdown must not wedge on a slow drain.
1848
+ */
1849
+ async function closeClient() {
1850
+ try {
1851
+ if (opened.driver === "postgres") await opened.client.end({ timeout: 5 });
1852
+ else await opened.client.close();
1853
+ } catch {}
1854
+ }
1855
+ /**
1856
+ * The GitHub login-gate condition, in one leaf module. `auth.ts` derives its
1857
+ * load-time `authEnabled` value from this, and server boot calls it before any
1858
+ * migration or scheduler work.
1859
+ *
1860
+ * The gate is ON exactly when a complete GitHub OAuth app is configured. Open
1861
+ * mode requires both values to be absent; a partial configuration fails closed.
1862
+ */
1863
+ function loginGateEnabled() {
1864
+ const hasClientId = !!process.env.GITHUB_CLIENT_ID?.trim();
1865
+ if (hasClientId !== !!process.env.GITHUB_CLIENT_SECRET?.trim()) throw new Error("incomplete GitHub auth configuration: set both GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET, or unset both for open mode");
1866
+ return hasClientId;
1867
+ }
1868
+ /**
1869
+ * Data-access layer over Drizzle — replaces c0's file-per-loop store. The API is
1870
+ * function-style; persistence is relational (loops and runs are separate tables,
1871
+ * and `owner: PeerRef` is gone → `userId` + `machineId`).
1872
+ *
1873
+ * Under Postgres (postgres-js / pglite) the drizzle session is ASYNC, so every
1874
+ * DB-touching function returns a Promise. The 5 pure, DB-free helpers stay sync.
1875
+ * Using Drizzle (not raw SQL) keeps the dialect swap a swap, not a rewrite.
1876
+ */
1877
+ function newLoopId() {
1878
+ return `loop-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
1879
+ }
1880
+ function nowIso() {
1881
+ return (/* @__PURE__ */ new Date()).toISOString();
1882
+ }
1883
+ function nextCronAt(cron, timezone, after) {
1884
+ if (!timezone) throw new Error("invariant: cron schedule has no timezone");
1885
+ const probe = new E(cron, {
1886
+ paused: true,
1887
+ timezone
1888
+ });
1889
+ try {
1890
+ const next = probe.nextRun(new Date(after));
1891
+ if (!next) throw new Error(`cron expression never fires again: ${cron}`);
1892
+ return next.toISOString();
1893
+ } finally {
1894
+ probe.stop();
1895
+ }
1896
+ }
1897
+ function runTimes(at = nowIso()) {
1898
+ return {
1899
+ ts: at,
1900
+ createdAt: at,
1901
+ updatedAt: at
1902
+ };
1903
+ }
1904
+ async function listLoops() {
1905
+ return db.select().from(loops);
1906
+ }
1907
+ async function listLoopsForUser(userId) {
1908
+ return db.select().from(loops).where(eq(loops.userId, userId));
1909
+ }
1910
+ async function getLoop(id) {
1911
+ return (await db.select().from(loops).where(eq(loops.id, id)))[0];
1912
+ }
1913
+ async function loopsForMachine(machineId) {
1914
+ return db.select().from(loops).where(eq(loops.machineId, machineId));
1915
+ }
1916
+ async function createLoop(input) {
1917
+ const ts = nowIso();
1918
+ const enabled = input.enabled ?? true;
1919
+ if (input.scheduleMode === "cron" && !input.timezone) throw new Error("invariant: cron loop creation requires a timezone");
1920
+ if (input.scheduleMode === "continuous" && input.timezone !== null) throw new Error("invariant: continuous loop creation requires a null timezone");
1921
+ const nextCadenceAt = !enabled ? null : input.scheduleMode === "continuous" ? ts : nextCronAt(input.cron, input.timezone, ts);
1922
+ const row = {
1923
+ ...input,
1924
+ enabled,
1925
+ pauseCause: input.pauseCause ?? (!enabled ? {
1926
+ kind: "owner",
1927
+ at: ts
1928
+ } : null),
1929
+ nextRunAt: enabled ? input.nextRunAt ?? null : null,
1930
+ nextCadenceAt,
1931
+ id: input.id ?? newLoopId(),
1932
+ createdAt: ts,
1933
+ updatedAt: ts
1934
+ };
1935
+ return db.transaction(async (tx) => {
1936
+ const machine = (await tx.select({
1937
+ id: machines.id,
1938
+ userId: machines.userId
1939
+ }).from(machines).where(eq(machines.id, input.machineId)).for("key share"))[0];
1940
+ if (!machine) throw new Error(`machine ${input.machineId} does not exist`);
1941
+ if (machine.userId !== input.userId) throw new Error(`machine ${input.machineId} is owned by a different user`);
1942
+ return (await tx.insert(loops).values(row).returning())[0];
1943
+ });
1944
+ }
1945
+ /** Allocate the next 1-based history number. Every caller already holds the
1946
+ * owning loop row lock, so the counter and run transition share one commit. */
1947
+ async function allocateRunIndexTx(tx, loopId) {
1948
+ const updated = (await tx.update(loops).set({ lastRunIndex: sql`${loops.lastRunIndex} + 1` }).where(eq(loops.id, loopId)).returning({ runIndex: loops.lastRunIndex }))[0];
1949
+ if (!updated) throw new Error(`cannot allocate history index for missing loop ${loopId}`);
1950
+ return updated.runIndex;
1951
+ }
1952
+ async function ensureRunIndexTx(tx, run) {
1953
+ return run.runIndex ?? allocateRunIndexTx(tx, run.loopId);
1954
+ }
1955
+ async function cancelPendingTx(tx, where, message, at) {
1956
+ const pending = await tx.select({
1957
+ id: runs.id,
1958
+ loopId: runs.loopId,
1959
+ runIndex: runs.runIndex
1960
+ }).from(runs).where(where).orderBy(asc(runs.createdAt), asc(runs.id)).for("update");
1961
+ for (const run of pending) {
1962
+ const runIndex = await ensureRunIndexTx(tx, run);
1963
+ await tx.update(runs).set({
1964
+ phase: "canceled",
1965
+ runIndex,
1966
+ message,
1967
+ ts: at,
1968
+ updatedAt: at
1969
+ }).where(and(eq(runs.id, run.id), eq(runs.phase, "pending")));
1970
+ }
1971
+ }
1972
+ /** Apply one loop patch while its row lock is held. This is shared by owner edits
1973
+ * and run-authorized mutations, so cadence semantics cannot drift. */
1974
+ async function updateLoopTx(tx, current, patch, at) {
1975
+ const extra = {};
1976
+ if (patch.enabled === true) extra.pauseCause = null;
1977
+ else if (patch.enabled === false && current.enabled && patch.pauseCause === void 0) extra.pauseCause = {
1978
+ kind: "owner",
1979
+ at
1980
+ };
1981
+ const effective = {
1982
+ ...current,
1983
+ ...patch,
1984
+ ...extra
1985
+ };
1986
+ if (current.deleteRequestedAt) extra.enabled = false;
1987
+ const pausing = !effective.enabled || current.deleteRequestedAt != null;
1988
+ const activating = effective.enabled && (patch.enabled === true && !current.enabled || patch.scheduleMode !== void 0 && patch.scheduleMode !== current.scheduleMode);
1989
+ if (pausing) {
1990
+ extra.nextCadenceAt = null;
1991
+ extra.nextRunAt = null;
1992
+ } else if (activating) if (effective.scheduleMode === "cron") extra.nextCadenceAt = nextCronAt(effective.cron, effective.timezone, at);
1993
+ else extra.nextCadenceAt = (await tx.select({ id: runs.id }).from(runs).where(and(eq(runs.loopId, current.id), inArray(runs.phase, ["pending", "running"]))).limit(1))[0] ? null : at;
1994
+ else if (effective.enabled && effective.scheduleMode === "cron" && (patch.cron !== void 0 && patch.cron !== current.cron || patch.timezone !== void 0 && patch.timezone !== current.timezone)) extra.nextCadenceAt = nextCronAt(effective.cron, effective.timezone, at);
1995
+ else if (effective.enabled && effective.scheduleMode === "continuous" && current.nextCadenceAt && Date.parse(current.nextCadenceAt) > Date.parse(at) && patch.continuousDelayMinutes !== void 0 && patch.continuousDelayMinutes !== current.continuousDelayMinutes) {
1996
+ const terminalAt = Date.parse(current.nextCadenceAt) - current.continuousDelayMinutes * 6e4;
1997
+ extra.nextCadenceAt = new Date(terminalAt + effective.continuousDelayMinutes * 6e4).toISOString();
1998
+ }
1999
+ const loop = (await tx.update(loops).set({
2000
+ ...patch,
2001
+ ...extra,
2002
+ updatedAt: at
2003
+ }).where(eq(loops.id, current.id)).returning())[0];
2004
+ if (patch.enabled === false) await cancelPendingTx(tx, and(eq(runs.loopId, current.id), eq(runs.phase, "pending"), eq(runs.requestedBy, "system")), "canceled - loop paused before this system run was claimed", at);
2005
+ if (patch.artifacts !== void 0) {
2006
+ const allowed = new Set(patch.artifacts ?? []);
2007
+ const removed = (await tx.select({
2008
+ id: artifactFiles.id,
2009
+ path: artifactFiles.path
2010
+ }).from(artifactFiles).where(and(eq(artifactFiles.loopId, current.id), eq(artifactFiles.deleted, false)))).filter((file) => !allowed.has(file.path)).map((file) => file.id);
2011
+ if (removed.length) await tx.update(artifactFiles).set({
2012
+ hash: null,
2013
+ deleted: true,
2014
+ updatedAt: at
2015
+ }).where(inArray(artifactFiles.id, removed));
2016
+ }
2017
+ return loop;
2018
+ }
2019
+ async function updateLoop(id, patch) {
2020
+ return db.transaction(async (tx) => {
2021
+ const current = (await tx.select().from(loops).where(eq(loops.id, id)).for("update"))[0];
2022
+ return current ? updateLoopTx(tx, current, patch, nowIso()) : void 0;
2023
+ });
2024
+ }
2025
+ /** Delete loop-owned history/content without erasing durable late-report evidence.
2026
+ * A retired lease is non-authorizing and never blocks deletion; only the matching
2027
+ * report→410 receipt transaction may consume it. */
2028
+ async function deleteLoopDataTx(tx, id) {
2029
+ if (!(await tx.delete(loops).where(eq(loops.id, id)).returning({ id: loops.id })).length) return false;
2030
+ await tx.delete(runs).where(eq(runs.loopId, id));
2031
+ await tx.delete(runLeases).where(and(eq(runLeases.loopId, id), ne(runLeases.state, "retired")));
2032
+ await tx.delete(artifactFiles).where(eq(artifactFiles.loopId, id));
2033
+ await tx.delete(runSnapshots).where(eq(runSnapshots.loopId, id));
2034
+ return true;
2035
+ }
2036
+ /** Stable Pause plus the post-transition running fact, observed under the same loop
2037
+ * lock. Callers use this result for truthful wording without a pre-lock race. */
2038
+ async function pauseLoopState(id) {
2039
+ return db.transaction(async (tx) => {
2040
+ const current = (await tx.select().from(loops).where(eq(loops.id, id)).for("update"))[0];
2041
+ if (!current) return void 0;
2042
+ const loop = await updateLoopTx(tx, current, { enabled: false }, nowIso());
2043
+ const running = (await tx.select().from(runs).where(and(eq(runs.loopId, id), eq(runs.phase, "running"))).limit(1))[0];
2044
+ return {
2045
+ loop,
2046
+ ...running ? { running } : {}
2047
+ };
2048
+ });
2049
+ }
2050
+ async function pauseLoop(id) {
2051
+ return (await pauseLoopState(id))?.loop;
2052
+ }
2053
+ async function startLoop(id) {
2054
+ return db.transaction(async (tx) => {
2055
+ const current = (await tx.select().from(loops).where(eq(loops.id, id)).for("update"))[0];
2056
+ if (!current || current.deleteRequestedAt) return void 0;
2057
+ return updateLoopTx(tx, current, { enabled: true }, nowIso());
2058
+ });
2059
+ }
2060
+ async function stopLoopTx(tx, current, at) {
2061
+ const loop = (await tx.update(loops).set({
2062
+ enabled: false,
2063
+ pauseCause: {
2064
+ kind: "owner",
2065
+ at
2066
+ },
2067
+ nextCadenceAt: null,
2068
+ nextRunAt: null,
2069
+ updatedAt: at
2070
+ }).where(eq(loops.id, current.id)).returning())[0];
2071
+ await cancelPendingTx(tx, and(eq(runs.loopId, current.id), eq(runs.phase, "pending")), "canceled - loop stopped before this queued run was claimed", at);
2072
+ const running = (await tx.select().from(runs).where(and(eq(runs.loopId, current.id), eq(runs.phase, "running"))).limit(1).for("update"))[0];
2073
+ if (!running) return { loop };
2074
+ return {
2075
+ loop,
2076
+ running: running.cancelRequestedAt ? running : (await tx.update(runs).set({
2077
+ cancelRequestedAt: at,
2078
+ updatedAt: at
2079
+ }).where(and(eq(runs.id, running.id), eq(runs.phase, "running"), isNull(runs.cancelRequestedAt))).returning())[0] ?? running
2080
+ };
2081
+ }
2082
+ async function stopLoop(id) {
2083
+ return db.transaction(async (tx) => {
2084
+ const current = (await tx.select().from(loops).where(eq(loops.id, id)).for("update"))[0];
2085
+ return current ? stopLoopTx(tx, current, nowIso()) : void 0;
2086
+ });
2087
+ }
2088
+ async function requestRunCancel(loopId, runId) {
2089
+ return db.transaction(async (tx) => {
2090
+ if (!(await tx.select({ id: loops.id }).from(loops).where(eq(loops.id, loopId)).for("update"))[0]) return void 0;
2091
+ const run = (await tx.select().from(runs).where(and(eq(runs.id, runId), eq(runs.loopId, loopId))).limit(1).for("update"))[0];
2092
+ if (!run) return void 0;
2093
+ if (run.phase === "pending") {
2094
+ const at = nowIso();
2095
+ const runIndex = await ensureRunIndexTx(tx, run);
2096
+ return (await tx.update(runs).set({
2097
+ phase: "canceled",
2098
+ runIndex,
2099
+ error: "stopped by user",
2100
+ ts: at,
2101
+ updatedAt: at
2102
+ }).where(and(eq(runs.id, runId), eq(runs.phase, "pending"))).returning())[0] ?? run;
2103
+ }
2104
+ if (run.phase === "running" && !run.cancelRequestedAt) {
2105
+ const at = nowIso();
2106
+ return (await tx.update(runs).set({
2107
+ cancelRequestedAt: at,
2108
+ updatedAt: at
2109
+ }).where(and(eq(runs.id, runId), eq(runs.phase, "running"), isNull(runs.cancelRequestedAt))).returning())[0] ?? run;
2110
+ }
2111
+ return run;
2112
+ });
2113
+ }
2114
+ async function requestDeleteLoop(id) {
2115
+ return db.transaction(async (tx) => {
2116
+ let current = (await tx.select().from(loops).where(eq(loops.id, id)).for("update"))[0];
2117
+ if (!current) return void 0;
2118
+ const at = current.deleteRequestedAt ?? nowIso();
2119
+ if (!current.deleteRequestedAt) current = (await tx.update(loops).set({
2120
+ deleteRequestedAt: at,
2121
+ updatedAt: at
2122
+ }).where(eq(loops.id, id)).returning())[0];
2123
+ return stopLoopTx(tx, current, at);
2124
+ });
2125
+ }
2126
+ async function tryDeleteLoop(id) {
2127
+ return db.transaction(async (tx) => {
2128
+ if (!(await tx.select().from(loops).where(eq(loops.id, id)).for("update"))[0]?.deleteRequestedAt) return false;
2129
+ const open = (await tx.select({ id: runs.id }).from(runs).where(and(eq(runs.loopId, id), inArray(runs.phase, ["pending", "running"]))).limit(1))[0];
2130
+ const authority = (await tx.select({ id: runLeases.tokenHash }).from(runLeases).where(and(eq(runLeases.loopId, id), inArray(runLeases.state, ["active", "terminal-grace"]))).limit(1))[0];
2131
+ if (open || authority) return false;
2132
+ await tx.update(runLeases).set({
2133
+ state: "retired",
2134
+ expiresAt: null
2135
+ }).where(and(eq(runLeases.loopId, id), eq(runLeases.state, "reconciliation-only")));
2136
+ return deleteLoopDataTx(tx, id);
2137
+ });
2138
+ }
2139
+ async function forceDeleteLoopTx(tx, id) {
2140
+ if (!(await tx.select().from(loops).where(eq(loops.id, id)).for("update"))[0]) return false;
2141
+ await tx.update(runLeases).set({
2142
+ state: "retired",
2143
+ expiresAt: null
2144
+ }).where(and(eq(runLeases.loopId, id), inArray(runLeases.state, [
2145
+ "active",
2146
+ "terminal-grace",
2147
+ "reconciliation-only",
2148
+ "retired"
2149
+ ])));
2150
+ return deleteLoopDataTx(tx, id);
2151
+ }
2152
+ async function forceDeleteLoop(id) {
2153
+ return db.transaction((tx) => forceDeleteLoopTx(tx, id));
2154
+ }
2155
+ async function enqueueRunTx(tx, loop, request, at) {
2156
+ if (loop.deleteRequestedAt) return {
2157
+ state: "rejected",
2158
+ reason: "loop is being deleted"
2159
+ };
2160
+ if (request.requestedBy !== "owner" && !loop.enabled) return {
2161
+ state: "skipped",
2162
+ reason: "loop is paused"
2163
+ };
2164
+ const pending = (await tx.select().from(runs).where(and(eq(runs.loopId, loop.id), eq(runs.phase, "pending"))).limit(1).for("update"))[0];
2165
+ if (pending) {
2166
+ const requestedBy = pending.requestedBy === "owner" || request.requestedBy === "owner" ? "owner" : "system";
2167
+ const updated = (await tx.update(runs).set({
2168
+ requestedBy,
2169
+ ts: at,
2170
+ updatedAt: at
2171
+ }).where(and(eq(runs.id, pending.id), eq(runs.phase, "pending"))).returning())[0];
2172
+ return updated ? {
2173
+ state: "coalesced",
2174
+ run: updated
2175
+ } : {
2176
+ state: "skipped",
2177
+ reason: "queued run was claimed concurrently"
2178
+ };
2179
+ }
2180
+ const row = {
2181
+ id: randomUUID(),
2182
+ loopId: loop.id,
2183
+ machineId: loop.machineId,
2184
+ phase: "pending",
2185
+ requestedBy: request.requestedBy,
2186
+ ...runTimes(at)
2187
+ };
2188
+ return {
2189
+ state: "queued",
2190
+ run: (await tx.insert(runs).values(row).returning())[0]
2191
+ };
2192
+ }
2193
+ async function failureStreakTx(tx, loopId) {
2194
+ const lastOk = (await tx.select({ runIndex: runs.runIndex }).from(runs).where(and(eq(runs.loopId, loopId), eq(runs.phase, "done"), isNotNull(runs.runIndex))).orderBy(desc(runs.runIndex)).limit(1))[0];
2195
+ const conds = [
2196
+ eq(runs.loopId, loopId),
2197
+ eq(runs.phase, "error"),
2198
+ isNotNull(runs.runIndex)
2199
+ ];
2200
+ if (lastOk?.runIndex != null) conds.push(gt(runs.runIndex, lastOk.runIndex));
2201
+ const row = (await tx.select({ n: sql`count(*)` }).from(runs).where(and(...conds)))[0];
2202
+ return Number(row?.n ?? 0);
2203
+ }
2204
+ async function terminalLifecycleTx(tx, currentLoop, run, terminalAt, loopPatch = {}, failureAutopauseStreak = 0) {
2205
+ const effective = {
2206
+ ...currentLoop,
2207
+ ...loopPatch
2208
+ };
2209
+ const update = { ...loopPatch };
2210
+ if ((run.phase === "done" || run.phase === "error") && effective.scheduleMode === "continuous") {
2211
+ const openExec = (await tx.select({ id: runs.id }).from(runs).where(and(eq(runs.loopId, currentLoop.id), inArray(runs.phase, ["pending", "running"]))).limit(1))[0];
2212
+ update.nextCadenceAt = effective.enabled && !openExec ? new Date(Date.parse(terminalAt) + Math.max(1, effective.continuousDelayMinutes) * 6e4).toISOString() : null;
2213
+ }
2214
+ let loop = Object.keys(update).length ? (await tx.update(loops).set({
2215
+ ...update,
2216
+ updatedAt: terminalAt
2217
+ }).where(eq(loops.id, currentLoop.id)).returning())[0] : currentLoop;
2218
+ let failureStreak = 0;
2219
+ let autoPaused = false;
2220
+ if (run.phase === "done" && run.status === "block") {
2221
+ loop = (await tx.update(loops).set({
2222
+ enabled: false,
2223
+ pauseCause: {
2224
+ kind: "blocked",
2225
+ at: terminalAt,
2226
+ runId: run.id
2227
+ },
2228
+ nextCadenceAt: null,
2229
+ nextRunAt: null,
2230
+ updatedAt: terminalAt
2231
+ }).where(eq(loops.id, currentLoop.id)).returning())[0];
2232
+ await cancelPendingTx(tx, and(eq(runs.loopId, currentLoop.id), eq(runs.phase, "pending"), eq(runs.requestedBy, "system")), "canceled - loop auto-paused after a block report", terminalAt);
2233
+ autoPaused = true;
2234
+ }
2235
+ if (run.phase === "error") {
2236
+ failureStreak = await failureStreakTx(tx, currentLoop.id);
2237
+ if (failureAutopauseStreak > 0 && failureStreak >= failureAutopauseStreak && loop.enabled) {
2238
+ loop = (await tx.update(loops).set({
2239
+ enabled: false,
2240
+ pauseCause: {
2241
+ kind: "failure-streak",
2242
+ at: terminalAt,
2243
+ runId: run.id,
2244
+ count: failureStreak
2245
+ },
2246
+ nextCadenceAt: null,
2247
+ nextRunAt: null,
2248
+ updatedAt: terminalAt
2249
+ }).where(eq(loops.id, currentLoop.id)).returning())[0];
2250
+ await cancelPendingTx(tx, and(eq(runs.loopId, currentLoop.id), eq(runs.phase, "pending"), eq(runs.requestedBy, "system")), "canceled - loop auto-paused after consecutive failures", terminalAt);
2251
+ autoPaused = true;
2252
+ }
2253
+ }
2254
+ return {
2255
+ loop,
2256
+ failureStreak,
2257
+ autoPaused
2258
+ };
2259
+ }
2260
+ async function getRun(id) {
2261
+ return (await db.select().from(runs).where(eq(runs.id, id)))[0];
2262
+ }
2263
+ async function runningRunsForMachine(machineId) {
2264
+ return db.select().from(runs).where(and(eq(runs.machineId, machineId), eq(runs.phase, "running"))).orderBy(asc(runs.createdAt), asc(runs.id));
2265
+ }
2266
+ async function runningRunForLoop(loopId) {
2267
+ return (await db.select().from(runs).where(and(eq(runs.loopId, loopId), eq(runs.phase, "running"))).limit(1))[0];
2268
+ }
2269
+ async function getReportReceipt(reportId) {
2270
+ return (await db.select().from(runReportReceipts).where(eq(runReportReceipts.reportId, reportId)))[0];
2271
+ }
2272
+ function terminalIncidentReceiptId(reportId, payloadDigest) {
2273
+ return createHash("sha256").update(reportId + payloadDigest).digest("hex");
2274
+ }
2275
+ async function getTerminalReportIncidents(reportId) {
2276
+ return db.select().from(terminalReportIncidents).where(eq(terminalReportIncidents.reportId, reportId)).orderBy(asc(terminalReportIncidents.createdAt));
2277
+ }
2278
+ async function getExactTerminalReportIncident(reportId, payloadDigest) {
2279
+ return (await db.select().from(terminalReportIncidents).where(eq(terminalReportIncidents.id, terminalIncidentReceiptId(reportId, payloadDigest))))[0];
2280
+ }
2281
+ async function putTerminalReportIncidentIfAbsent(input) {
2282
+ return db.transaction(async (tx) => {
2283
+ await lockReportIdTx(tx, input.reportId);
2284
+ const id = terminalIncidentReceiptId(input.reportId, input.payloadDigest);
2285
+ const existing = (await tx.select().from(terminalReportIncidents).where(eq(terminalReportIncidents.id, id)).limit(1))[0];
2286
+ if (existing) return existing;
2287
+ return (await tx.insert(terminalReportIncidents).values({
2288
+ id,
2289
+ ...input,
2290
+ createdAt: nowIso()
2291
+ }).returning())[0];
2292
+ });
2293
+ }
2294
+ /** Serialize all terminal handling for one reportId, even when the competing
2295
+ * runs belong to different loop locks or one terminal result lands in the incident table. */
2296
+ async function lockReportIdTx(tx, reportId) {
2297
+ const unsigned = BigInt(`0x${createHash("sha256").update(reportId).digest("hex").slice(0, 16)}`);
2298
+ const key = BigInt.asIntN(64, unsigned).toString();
2299
+ await tx.execute(sql`select pg_advisory_xact_lock(${key}::bigint)`);
2300
+ }
2301
+ /** Race-safe receipt reservation for definitive non-finalization ACKs (RETIRED).
2302
+ * Returns the row that owns reportId, whether inserted here or concurrently. */
2303
+ async function putReportReceiptIfAbsent(input) {
2304
+ return db.transaction(async (tx) => {
2305
+ await lockReportIdTx(tx, input.reportId);
2306
+ return (await tx.insert(runReportReceipts).values(input).onConflictDoNothing({ target: runReportReceipts.reportId }).returning())[0] ?? (await tx.select().from(runReportReceipts).where(eq(runReportReceipts.reportId, input.reportId)))[0];
2307
+ });
2308
+ }
2309
+ /** Idempotent startup repair for a crash across terminalization.
2310
+ * Terminal runs can never retain active authority. */
2311
+ async function repairTerminalRunLeases(now = Date.now()) {
2312
+ const repaired = await db.update(runLeases).set({
2313
+ state: "terminal-grace",
2314
+ expiresAt: new Date(now + TERMINAL_REPORT_GRACE_MS).toISOString()
2315
+ }).where(and(eq(runLeases.state, "active"), sql`exists (select 1 from ${runs} where ${runs.id} = ${runLeases.runId} and ${runs.phase} in ('done', 'error', 'canceled'))`)).returning({ tokenHash: runLeases.tokenHash });
2316
+ await db.update(runLeases).set({ expiresAt: null }).where(eq(runLeases.state, "retired"));
2317
+ return repaired.length;
2318
+ }
2319
+ /** Atomically persist a definitive 410 and consume only the matching retired
2320
+ * tombstone. If the HTTP response is lost, the independent receipt replays it. */
2321
+ async function acknowledgeRetiredReport(leaseTokenHash, input) {
2322
+ return db.transaction(async (tx) => {
2323
+ await lockReportIdTx(tx, input.reportId);
2324
+ if (!(await tx.select().from(runLeases).where(and(eq(runLeases.tokenHash, leaseTokenHash), eq(runLeases.state, "retired"))).limit(1).for("update"))[0]) return void 0;
2325
+ await tx.insert(runReportReceipts).values(input).onConflictDoNothing({ target: runReportReceipts.reportId });
2326
+ const stored = (await tx.select().from(runReportReceipts).where(eq(runReportReceipts.reportId, input.reportId)))[0];
2327
+ if (stored?.runId === input.runId && stored.payloadDigest === input.payloadDigest && stored.ackStatus === input.ackStatus) await tx.delete(runLeases).where(and(eq(runLeases.tokenHash, leaseTokenHash), eq(runLeases.state, "retired")));
2328
+ return stored;
2329
+ });
2330
+ }
2331
+ /** Refresh provider-neutral liveness in one machine-scoped UPDATE. Untrusted ids
2332
+ * cannot stamp another machine or a non-running row; fresh stamps stay untouched. */
2333
+ async function refreshRunHeartbeats(machineId, runIds, at, staleBefore) {
2334
+ if (!runIds.length) return 0;
2335
+ return (await db.update(runs).set({
2336
+ heartbeatAt: at,
2337
+ updatedAt: at
2338
+ }).where(and(eq(runs.machineId, machineId), eq(runs.phase, "running"), inArray(runs.id, runIds), or(isNull(runs.heartbeatAt), lt(runs.heartbeatAt, staleBefore)))).returning({ id: runs.id })).length;
2339
+ }
2340
+ async function activeRunForReportTx(tx, loopId, runId, leaseTokenHash) {
2341
+ const lease = (await tx.select().from(runLeases).where(and(eq(runLeases.tokenHash, leaseTokenHash), eq(runLeases.runId, runId), eq(runLeases.loopId, loopId), eq(runLeases.state, "active"))).limit(1).for("update"))[0];
2342
+ if (!lease) return { state: "invalid-lease" };
2343
+ const run = (await tx.select().from(runs).where(and(eq(runs.id, runId), eq(runs.loopId, loopId), eq(runs.machineId, lease.machineId), eq(runs.phase, "running"))).limit(1).for("update"))[0];
2344
+ return run ? {
2345
+ state: "active",
2346
+ run
2347
+ } : { state: "run-not-running" };
2348
+ }
2349
+ /** Record the agent-authored status/message exactly once. The loop row is the
2350
+ * serialization point, so sequential and concurrent duplicate callbacks both
2351
+ * lose before any field can be overwritten. */
2352
+ async function recordRunReportOnce(input) {
2353
+ return db.transaction(async (tx) => {
2354
+ if (!(await tx.select({ id: loops.id }).from(loops).where(eq(loops.id, input.loopId)).for("update"))[0]) return { state: "missing-loop" };
2355
+ const active = await activeRunForReportTx(tx, input.loopId, input.runId, input.leaseTokenHash);
2356
+ if (active.state !== "active") return active;
2357
+ if (active.run.status != null || active.run.message != null) return { state: "already-reported" };
2358
+ const at = nowIso();
2359
+ const run = (await tx.update(runs).set({
2360
+ status: input.status,
2361
+ message: input.message,
2362
+ updatedAt: at
2363
+ }).where(and(eq(runs.id, input.runId), eq(runs.loopId, input.loopId), eq(runs.phase, "running"), isNull(runs.status), isNull(runs.message))).returning())[0];
2364
+ return run ? {
2365
+ state: "applied",
2366
+ run
2367
+ } : { state: "already-reported" };
2368
+ });
2369
+ }
2370
+ /** Consume a retired tombstone when its reportId is owned by another run.
2371
+ * The run/loop may already be deleted, so only the exact incident receipt and
2372
+ * lease tombstone participate in this transaction. */
2373
+ async function acknowledgeRetiredTerminalIncident(input) {
2374
+ return db.transaction(async (tx) => {
2375
+ await lockReportIdTx(tx, input.reportId);
2376
+ const id = terminalIncidentReceiptId(input.reportId, input.payloadDigest);
2377
+ const replay = (await tx.select().from(terminalReportIncidents).where(eq(terminalReportIncidents.id, id)).limit(1))[0];
2378
+ const lease = (await tx.select({ tokenHash: runLeases.tokenHash }).from(runLeases).where(and(eq(runLeases.tokenHash, input.leaseTokenHash), eq(runLeases.runId, input.runId), eq(runLeases.state, "retired"))).limit(1).for("update"))[0];
2379
+ if (!lease) return void 0;
2380
+ if (replay) {
2381
+ await tx.delete(runLeases).where(eq(runLeases.tokenHash, lease.tokenHash));
2382
+ return replay;
2383
+ }
2384
+ const receipt = (await tx.insert(terminalReportIncidents).values({
2385
+ id,
2386
+ runId: input.runId,
2387
+ reportId: input.reportId,
2388
+ payloadDigest: input.payloadDigest,
2389
+ disposition: "telemetry-rejected",
2390
+ ackBody: input.ackBody,
2391
+ createdAt: nowIso()
2392
+ }).returning())[0];
2393
+ await tx.delete(runLeases).where(eq(runLeases.tokenHash, lease.tokenHash));
2394
+ return receipt;
2395
+ });
2396
+ }
2397
+ /** Reject one correlatable terminal attempt as a durable terminal fact. Authority,
2398
+ * run/loop lifecycle, incident receipt, and lease consumption share one transaction.
2399
+ * No payload-authored cursor/task/message field is accepted here. */
2400
+ async function rejectTerminalReport(input) {
2401
+ return db.transaction(async (tx) => {
2402
+ await lockReportIdTx(tx, input.reportId);
2403
+ const current = (await tx.select().from(loops).where(eq(loops.id, input.loopId)).for("update"))[0];
2404
+ if (!current) return { state: "missing-loop" };
2405
+ const normal = (await tx.select().from(runReportReceipts).where(eq(runReportReceipts.reportId, input.reportId)))[0];
2406
+ if (normal?.runId === input.runId) return {
2407
+ state: "normal-replay",
2408
+ receipt: normal
2409
+ };
2410
+ const priorIncident = (await tx.select().from(terminalReportIncidents).where(eq(terminalReportIncidents.id, terminalIncidentReceiptId(input.reportId, input.payloadDigest))).limit(1))[0];
2411
+ if (priorIncident?.runId === input.runId) return {
2412
+ state: "incident-replay",
2413
+ receipt: priorIncident
2414
+ };
2415
+ const now = nowIso();
2416
+ const leaseConditions = [
2417
+ eq(runLeases.tokenHash, input.leaseTokenHash),
2418
+ eq(runLeases.runId, input.runId),
2419
+ eq(runLeases.loopId, input.loopId),
2420
+ input.leaseState === "active" ? eq(runLeases.state, "active") : inArray(runLeases.state, ["terminal-grace", "reconciliation-only"])
2421
+ ];
2422
+ if (input.leaseState !== "active") leaseConditions.push(gt(runLeases.expiresAt, now));
2423
+ const lease = (await tx.select().from(runLeases).where(and(...leaseConditions)).limit(1).for("update"))[0];
2424
+ if (!lease) return { state: "invalid-lease" };
2425
+ const receiptInput = {
2426
+ id: terminalIncidentReceiptId(input.reportId, input.payloadDigest),
2427
+ runId: input.runId,
2428
+ reportId: input.reportId,
2429
+ payloadDigest: input.payloadDigest,
2430
+ disposition: input.disposition,
2431
+ ackBody: input.ackBody,
2432
+ createdAt: input.incident.at
2433
+ };
2434
+ if (lease.state !== "active") {
2435
+ const run = (await tx.update(runs).set({
2436
+ reportIncident: input.incident,
2437
+ updatedAt: input.incident.at
2438
+ }).where(and(eq(runs.id, input.runId), eq(runs.loopId, input.loopId), inArray(runs.phase, [
2439
+ "done",
2440
+ "error",
2441
+ "canceled"
2442
+ ]))).returning())[0];
2443
+ if (!run) return { state: "run-not-terminalizable" };
2444
+ await tx.delete(runLeases).where(eq(runLeases.tokenHash, input.leaseTokenHash));
2445
+ return {
2446
+ state: "telemetry-rejected",
2447
+ run,
2448
+ loop: current,
2449
+ receipt: priorIncident ?? (await tx.insert(terminalReportIncidents).values(receiptInput).returning())[0],
2450
+ failureStreak: 0,
2451
+ autoPaused: false
2452
+ };
2453
+ }
2454
+ const target = (await tx.select().from(runs).where(and(eq(runs.id, input.runId), eq(runs.loopId, input.loopId), eq(runs.phase, "running"))).limit(1).for("update"))[0];
2455
+ if (!target) return { state: "run-not-terminalizable" };
2456
+ const runIndex = await ensureRunIndexTx(tx, target);
2457
+ const run = (await tx.update(runs).set({
2458
+ phase: "error",
2459
+ runIndex,
2460
+ error: input.incident.reason,
2461
+ reportIncident: input.incident,
2462
+ ts: input.incident.at,
2463
+ updatedAt: input.incident.at
2464
+ }).where(and(eq(runs.id, input.runId), eq(runs.loopId, input.loopId), eq(runs.phase, "running"))).returning())[0];
2465
+ if (!run) return { state: "run-not-terminalizable" };
2466
+ const lifecycle = await terminalLifecycleTx(tx, current, run, input.incident.at, {}, input.failureAutopauseStreak ?? 0);
2467
+ await tx.delete(runLeases).where(eq(runLeases.tokenHash, input.leaseTokenHash));
2468
+ const receipt = priorIncident ?? (await tx.insert(terminalReportIncidents).values(receiptInput).returning())[0];
2469
+ return {
2470
+ state: "run-error",
2471
+ run,
2472
+ ...lifecycle,
2473
+ receipt
2474
+ };
2475
+ });
2476
+ }
2477
+ /** Atomically CAS running→terminal and apply the report's loop patch.
2478
+ * The loop patch is unreachable when cancel/reclaim/another report won. */
2479
+ async function finalizeRunningRun(loopId, runId, runPatch, loopPatch = {}, leaseTokenHash, failureAutopauseStreak = 0, receipt) {
2480
+ return db.transaction(async (tx) => {
2481
+ if (receipt) await lockReportIdTx(tx, receipt.reportId);
2482
+ const current = (await tx.select().from(loops).where(eq(loops.id, loopId)).for("update"))[0];
2483
+ if (!current) return void 0;
2484
+ if (receipt) {
2485
+ if ((await tx.select({ id: terminalReportIncidents.id }).from(terminalReportIncidents).where(eq(terminalReportIncidents.reportId, receipt.reportId)).limit(1))[0]) return void 0;
2486
+ }
2487
+ if (leaseTokenHash) {
2488
+ if ((await activeRunForReportTx(tx, loopId, runId, leaseTokenHash)).state !== "active") return void 0;
2489
+ }
2490
+ const target = (await tx.select().from(runs).where(and(eq(runs.id, runId), eq(runs.loopId, loopId), eq(runs.phase, "running"))).limit(1).for("update"))[0];
2491
+ if (!target) return void 0;
2492
+ const runIndex = await ensureRunIndexTx(tx, target);
2493
+ const at = typeof runPatch.ts === "string" ? runPatch.ts : nowIso();
2494
+ const run = (await tx.update(runs).set({
2495
+ ...runPatch,
2496
+ runIndex,
2497
+ updatedAt: at
2498
+ }).where(and(eq(runs.id, runId), eq(runs.loopId, loopId), eq(runs.phase, "running"))).returning())[0];
2499
+ if (!run) return void 0;
2500
+ const lifecycle = await terminalLifecycleTx(tx, current, run, at, loopPatch, failureAutopauseStreak);
2501
+ if (leaseTokenHash) await tx.delete(runLeases).where(eq(runLeases.runId, runId));
2502
+ if (receipt) await tx.insert(runReportReceipts).values(receipt);
2503
+ return {
2504
+ run,
2505
+ ...lifecycle
2506
+ };
2507
+ });
2508
+ }
2509
+ /** Sweep reclaim under the same loop lock as claim/report/cancel. Running
2510
+ * reclaims terminalize their lease and write the provisional continuous cadence
2511
+ * fact atomically; pending reclaims have no lease. */
2512
+ async function reclaimRun(runId, expected, reason, at = nowIso(), graceMs = 1440 * 60 * 1e3, failureAutopauseStreak = 0) {
2513
+ return db.transaction(async (tx) => {
2514
+ const observed = (await tx.select({ loopId: runs.loopId }).from(runs).where(eq(runs.id, runId)).limit(1))[0];
2515
+ if (!observed) return void 0;
2516
+ const loop = (await tx.select().from(loops).where(eq(loops.id, observed.loopId)).for("update"))[0];
2517
+ if (!loop) return void 0;
2518
+ const currentRun = (await tx.select().from(runs).where(eq(runs.id, runId)).limit(1).for("update"))[0];
2519
+ if (currentRun?.phase !== expected) return void 0;
2520
+ if (expected === "running") await tx.update(runLeases).set({
2521
+ state: "terminal-grace",
2522
+ expiresAt: new Date(Date.parse(at) + graceMs).toISOString()
2523
+ }).where(and(eq(runLeases.runId, runId), eq(runLeases.state, "active")));
2524
+ const runIndex = await ensureRunIndexTx(tx, currentRun);
2525
+ const reclaimed = (await tx.update(runs).set({
2526
+ phase: "error",
2527
+ runIndex,
2528
+ error: reason,
2529
+ ts: at,
2530
+ updatedAt: at
2531
+ }).where(and(eq(runs.id, runId), eq(runs.phase, expected))).returning())[0];
2532
+ if (!reclaimed) return void 0;
2533
+ return {
2534
+ run: reclaimed,
2535
+ ...await terminalLifecycleTx(tx, loop, reclaimed, at, {}, failureAutopauseStreak)
2536
+ };
2537
+ });
2538
+ }
2539
+ /** A daemon's completed recovery snapshot is authoritative for execution
2540
+ * exclusion. Reclaimed runs absent from that snapshot retain late-report authority
2541
+ * until their original expiry, but stop blocking queued work immediately. */
2542
+ async function releaseAbsentReconciliations(machineId, recoverableRunIds, at = nowIso()) {
2543
+ const conditions = [
2544
+ eq(runLeases.machineId, machineId),
2545
+ eq(runLeases.state, "terminal-grace"),
2546
+ gt(runLeases.expiresAt, at)
2547
+ ];
2548
+ const ids = [...new Set(recoverableRunIds)];
2549
+ if (ids.length) conditions.push(notInArray(runLeases.runId, ids));
2550
+ return (await db.update(runLeases).set({ state: "reconciliation-only" }).where(and(...conditions)).returning({ runId: runLeases.runId })).map((row) => row.runId);
2551
+ }
2552
+ /** Consume one terminal reconciliation lease and reconcile its reclaimed error
2553
+ * exactly once. Keeping the lease check/delete beside the run + loop writes closes the
2554
+ * error→error double-report hole where a phase CAS alone cannot distinguish the
2555
+ * first real failure from a second concurrent one. */
2556
+ async function reconcileReclaimedRun(loopId, runId, leaseTokenHash, runPatch, loopPatch = {}, failureAutopauseStreak = 0, receipt) {
2557
+ return db.transaction(async (tx) => {
2558
+ if (receipt) await lockReportIdTx(tx, receipt.reportId);
2559
+ const current = (await tx.select().from(loops).where(eq(loops.id, loopId)).for("update"))[0];
2560
+ if (!current) return void 0;
2561
+ if (receipt) {
2562
+ if ((await tx.select({ id: terminalReportIncidents.id }).from(terminalReportIncidents).where(eq(terminalReportIncidents.reportId, receipt.reportId)).limit(1))[0]) return void 0;
2563
+ }
2564
+ const leaseNow = nowIso();
2565
+ const lease = (await tx.select({
2566
+ tokenHash: runLeases.tokenHash,
2567
+ state: runLeases.state
2568
+ }).from(runLeases).where(and(eq(runLeases.tokenHash, leaseTokenHash), eq(runLeases.runId, runId), eq(runLeases.loopId, loopId), inArray(runLeases.state, ["terminal-grace", "reconciliation-only"]), gt(runLeases.expiresAt, leaseNow))).limit(1).for("update"))[0];
2569
+ if (!lease) return void 0;
2570
+ const target = (await tx.select().from(runs).where(and(eq(runs.id, runId), eq(runs.loopId, loopId), eq(runs.phase, "error"))).limit(1).for("update"))[0];
2571
+ if (!target) return void 0;
2572
+ const runIndex = await ensureRunIndexTx(tx, target);
2573
+ const at = typeof runPatch.ts === "string" ? runPatch.ts : nowIso();
2574
+ const run = (await tx.update(runs).set({
2575
+ ...runPatch,
2576
+ runIndex,
2577
+ updatedAt: at
2578
+ }).where(and(eq(runs.id, runId), eq(runs.loopId, loopId), eq(runs.phase, "error"))).returning())[0];
2579
+ if (!run) return void 0;
2580
+ const lifecycle = lease.state === "reconciliation-only" ? {
2581
+ loop: current,
2582
+ failureStreak: 0,
2583
+ autoPaused: false
2584
+ } : await terminalLifecycleTx(tx, current, run, at, loopPatch, failureAutopauseStreak);
2585
+ await tx.delete(runLeases).where(eq(runLeases.runId, runId));
2586
+ if (receipt) await tx.insert(runReportReceipts).values(receipt);
2587
+ return {
2588
+ run,
2589
+ ...lifecycle,
2590
+ reportOnly: lease.state === "reconciliation-only"
2591
+ };
2592
+ });
2593
+ }
2594
+ var TERMINAL_REPORT_GRACE_MS = 600 * 1e3;
2595
+ /**
2596
+ * The durable run-queue write seam. It serializes on the loop row and owns every
2597
+ * queue invariant callers would otherwise have to reproduce:
2598
+ * - at most one ordinary pending row per loop (also backed by a partial unique index),
2599
+ * - a running run may retain one coalesced follow-up,
2600
+ * - owner authority promotes an existing system row and never downgrades,
2601
+ * - paused loops accept owner Run-once work while recurring system work stays stopped.
2602
+ */
2603
+ async function enqueueRun(loopId, request) {
2604
+ return db.transaction(async (tx) => {
2605
+ const loop = (await tx.select().from(loops).where(eq(loops.id, loopId)).for("update"))[0];
2606
+ if (!loop) return {
2607
+ state: "rejected",
2608
+ reason: "loop not found"
2609
+ };
2610
+ return enqueueRunTx(tx, loop, request, nowIso());
2611
+ });
2612
+ }
2613
+ /** Materialize every due schedule fact. Each loop is locked and rechecked in its
2614
+ * own transaction; cadence and one-shot facts may coalesce into one system exec
2615
+ * and are consumed together. */
2616
+ async function advanceDueSchedules(at = nowIso(), filter = {}) {
2617
+ const due = or(and(isNotNull(loops.nextCadenceAt), sql`${loops.nextCadenceAt} <= ${at}`), and(isNotNull(loops.nextRunAt), sql`${loops.nextRunAt} <= ${at}`));
2618
+ const candidates = await db.select({ id: loops.id }).from(loops).where(and(eq(loops.enabled, true), isNull(loops.deleteRequestedAt), due, filter.loopId ? eq(loops.id, filter.loopId) : void 0, filter.machineId ? eq(loops.machineId, filter.machineId) : void 0));
2619
+ const advanced = [];
2620
+ for (const { id } of candidates) {
2621
+ const result = await db.transaction(async (tx) => {
2622
+ const loop = (await tx.select().from(loops).where(eq(loops.id, id)).for("update"))[0];
2623
+ if (!loop?.enabled || loop.deleteRequestedAt) return void 0;
2624
+ const cadenceDue = !!loop.nextCadenceAt && Date.parse(loop.nextCadenceAt) <= Date.parse(at);
2625
+ const oneShotDue = !!loop.nextRunAt && Date.parse(loop.nextRunAt) <= Date.parse(at);
2626
+ if (!cadenceDue && !oneShotDue) return void 0;
2627
+ if ((await tx.select({ tokenHash: runLeases.tokenHash }).from(runLeases).where(and(eq(runLeases.loopId, id), eq(runLeases.state, "terminal-grace"), gt(runLeases.expiresAt, at))).limit(1))[0]) return void 0;
2628
+ const open = (await tx.select({ id: runs.id }).from(runs).where(and(eq(runs.loopId, id), inArray(runs.phase, ["pending", "running"]))).limit(1))[0];
2629
+ const skipRecurring = cadenceDue && !!open && (loop.scheduleMode === "continuous" || loop.cronOverlap === "skip");
2630
+ const queued = oneShotDue || !skipRecurring ? await enqueueRunTx(tx, loop, { requestedBy: "system" }, at) : void 0;
2631
+ if (queued && !("run" in queued)) return void 0;
2632
+ const nextCadenceAt = cadenceDue ? loop.scheduleMode === "cron" ? nextCronAt(loop.cron, loop.timezone, at) : null : loop.nextCadenceAt;
2633
+ const updatedLoop = (await tx.update(loops).set({
2634
+ nextCadenceAt,
2635
+ ...oneShotDue ? { nextRunAt: null } : {},
2636
+ updatedAt: at
2637
+ }).where(eq(loops.id, id)).returning())[0];
2638
+ return queued ? {
2639
+ loop: updatedLoop,
2640
+ run: queued.run,
2641
+ state: queued.state
2642
+ } : {
2643
+ loop: updatedLoop,
2644
+ state: "skipped"
2645
+ };
2646
+ });
2647
+ if (result) advanced.push(result);
2648
+ }
2649
+ return advanced;
2650
+ }
2651
+ /** Conservative boot initialization for pre-fact cron rows. No history lookup and
2652
+ * no missed-fire catch-up: only install the next future occurrence. */
2653
+ async function initializeCronCadence(at = nowIso()) {
2654
+ const candidates = await db.select({ id: loops.id }).from(loops).where(and(eq(loops.enabled, true), isNull(loops.deleteRequestedAt), eq(loops.scheduleMode, "cron"), isNull(loops.nextCadenceAt)));
2655
+ const initialized = [];
2656
+ for (const { id } of candidates) {
2657
+ const loop = await db.transaction(async (tx) => {
2658
+ const current = (await tx.select().from(loops).where(eq(loops.id, id)).for("update"))[0];
2659
+ if (!current?.enabled || current.scheduleMode !== "cron" || current.nextCadenceAt) return void 0;
2660
+ return (await tx.update(loops).set({
2661
+ nextCadenceAt: nextCronAt(current.cron, current.timezone, at),
2662
+ updatedAt: at
2663
+ }).where(eq(loops.id, id)).returning())[0];
2664
+ });
2665
+ if (loop) initialized.push(loop);
2666
+ }
2667
+ return initialized;
2668
+ }
2669
+ /** Claim one ready loop and mint its lease atomically. Repeated polls may add
2670
+ * unlimited cross-loop concurrency; the loop index remains the final same-loop
2671
+ * serialization invariant. Reported local runs exclude their loops even if the
2672
+ * server has already terminalized an older process. */
2673
+ async function claimReadyRunForMachine(machineId, at = nowIso(), excludeRunIds = []) {
2674
+ return db.transaction(async (tx) => {
2675
+ if (!(await tx.select({ id: machines.id }).from(machines).where(eq(machines.id, machineId)).for("update"))[0]) return void 0;
2676
+ const excludedLoops = /* @__PURE__ */ new Set();
2677
+ if (excludeRunIds.length) {
2678
+ const rows = await tx.select({ loopId: runs.loopId }).from(runs).where(and(eq(runs.machineId, machineId), inArray(runs.id, [...new Set(excludeRunIds)])));
2679
+ for (const row of rows) excludedLoops.add(row.loopId);
2680
+ }
2681
+ const next = (await tx.select({
2682
+ run: runs,
2683
+ loop: loops
2684
+ }).from(runs).innerJoin(loops, eq(loops.id, runs.loopId)).where(and(eq(runs.machineId, machineId), eq(runs.phase, "pending"), isNull(runs.cancelRequestedAt), or(eq(loops.enabled, true), eq(runs.requestedBy, "owner")), isNull(loops.deleteRequestedAt), excludedLoops.size ? notInArray(loops.id, [...excludedLoops]) : void 0, sql`not exists (select 1 from runs occupied where occupied.loop_id = ${loops.id} and occupied.phase = 'running')`, sql`not exists (select 1 from run_leases authority where authority.loop_id = ${loops.id} and (authority.state = 'active' or (authority.state = 'terminal-grace' and authority.expires_at > ${at})))`)).orderBy(asc(runs.createdAt), asc(runs.id)).limit(1))[0];
2685
+ if (!next) return void 0;
2686
+ let loop = (await tx.select().from(loops).where(eq(loops.id, next.loop.id)).for("update"))[0];
2687
+ const candidate = (await tx.select().from(runs).where(eq(runs.id, next.run.id)).limit(1).for("update"))[0];
2688
+ if (!loop || !candidate || candidate.phase !== "pending" || candidate.cancelRequestedAt || loop.deleteRequestedAt || !loop.enabled && candidate.requestedBy !== "owner") return void 0;
2689
+ const occupied = (await tx.select({ id: runs.id }).from(runs).where(and(eq(runs.loopId, loop.id), eq(runs.phase, "running"))).limit(1))[0];
2690
+ const authority = (await tx.select({ tokenHash: runLeases.tokenHash }).from(runLeases).where(and(eq(runLeases.loopId, loop.id), inArray(runLeases.state, ["active", "terminal-grace"]), or(eq(runLeases.state, "active"), gt(runLeases.expiresAt, at)))).limit(1))[0];
2691
+ if (occupied || authority) return void 0;
2692
+ const runIndex = await ensureRunIndexTx(tx, candidate);
2693
+ const run = (await tx.update(runs).set({
2694
+ phase: "running",
2695
+ runIndex,
2696
+ agent: loop.agent,
2697
+ model: loop.model,
2698
+ reasoningEffort: loop.reasoningEffort,
2699
+ heartbeatAt: null,
2700
+ ts: at,
2701
+ updatedAt: at
2702
+ }).where(and(eq(runs.id, candidate.id), eq(runs.phase, "pending"), isNull(runs.cancelRequestedAt))).returning())[0];
2703
+ if (!run) return void 0;
2704
+ if (loop.scheduleMode === "continuous" && loop.nextCadenceAt != null) loop = (await tx.update(loops).set({
2705
+ nextCadenceAt: null,
2706
+ updatedAt: at
2707
+ }).where(eq(loops.id, loop.id)).returning())[0];
2708
+ const runToken = `rk_${randomBytes(16).toString("hex")}`;
2709
+ const tokenHash = createHash("sha256").update(runToken).digest("hex");
2710
+ await tx.insert(runLeases).values({
2711
+ tokenHash,
2712
+ runId: run.id,
2713
+ loopId: loop.id,
2714
+ machineId,
2715
+ createdAt: at
2716
+ });
2717
+ return {
2718
+ run,
2719
+ loop,
2720
+ runToken
2721
+ };
2722
+ });
2723
+ }
2724
+ async function listRuns(loopId, limit = 30) {
2725
+ return (await db.select().from(runs).where(eq(runs.loopId, loopId)).orderBy(desc(runs.ts)).limit(limit)).reverse();
2726
+ }
2727
+ async function reconciliationStatesForRuns(loopId, runIds, at = nowIso()) {
2728
+ if (!runIds.length) return /* @__PURE__ */ new Map();
2729
+ const rows = await db.select({
2730
+ runId: runLeases.runId,
2731
+ state: runLeases.state
2732
+ }).from(runLeases).where(and(eq(runLeases.loopId, loopId), inArray(runLeases.runId, [...new Set(runIds)]), inArray(runLeases.state, ["terminal-grace", "reconciliation-only"]), gt(runLeases.expiresAt, at)));
2733
+ return new Map(rows.map((row) => [row.runId, row.state === "terminal-grace" ? "blocking" : "report-only"]));
2734
+ }
2735
+ /** One older page: runs strictly before `beforeTs`, newest-first then capped,
2736
+ * returned chronological (oldest-first) to match listRuns. Cursor-based (by ts,
2737
+ * not offset) so it's stable while new runs land at the head. */
2738
+ async function listRunsBefore(loopId, beforeTs, limit = 16) {
2739
+ return (await db.select().from(runs).where(and(eq(runs.loopId, loopId), lt(runs.ts, beforeTs))).orderBy(desc(runs.ts)).limit(limit)).reverse();
2740
+ }
2741
+ async function lastRun(loopId) {
2742
+ return (await db.select().from(runs).where(eq(runs.loopId, loopId)).orderBy(desc(runs.ts)).limit(1))[0];
2743
+ }
2744
+ async function countRuns(loopId) {
2745
+ const r = (await db.select({ n: sql`count(*)` }).from(runs).where(eq(runs.loopId, loopId)))[0];
2746
+ return Number(r?.n ?? 0);
2747
+ }
2748
+ async function recentLoopUsage(loopId, since) {
2749
+ const row = (await db.select({
2750
+ runCount: sql`count(*)`,
2751
+ tokenCount: sql`coalesce(sum(
2752
+ coalesce((${runs.usage} ->> 'inputTokens')::bigint, 0) +
2753
+ coalesce((${runs.usage} ->> 'outputTokens')::bigint, 0)
2754
+ ), 0)`
2755
+ }).from(runs).where(and(eq(runs.loopId, loopId), sql`${runs.ts} >= ${since}`)))[0];
2756
+ return {
2757
+ runCount: Number(row?.runCount ?? 0),
2758
+ tokenCount: Number(row?.tokenCount ?? 0)
2759
+ };
2760
+ }
2761
+ async function openRuns() {
2762
+ return db.select().from(runs).where(inArray(runs.phase, ["pending", "running"]));
2763
+ }
2764
+ async function hasRunningRun(loopId) {
2765
+ return !!(await db.select({ id: runs.id }).from(runs).where(and(eq(runs.loopId, loopId), eq(runs.phase, "running"))).limit(1))[0];
2766
+ }
2767
+ async function hasPendingRun(loopId) {
2768
+ return !!(await db.select({ id: runs.id }).from(runs).where(and(eq(runs.loopId, loopId), eq(runs.phase, "pending"))).limit(1))[0];
2769
+ }
2770
+ /** Expire one offline system row only if the sweep snapshot is still exact and
2771
+ * its immutable lifetime remains over the cap under the loop lock. */
2772
+ async function expirePendingRun(runId, expected, at, maxLifetimeMs, message) {
2773
+ return db.transaction(async (tx) => {
2774
+ const observed = (await tx.select({ loopId: runs.loopId }).from(runs).where(eq(runs.id, runId)).limit(1))[0];
2775
+ if (!observed) return false;
2776
+ const loop = (await tx.select({ id: loops.id }).from(loops).where(eq(loops.id, observed.loopId)).for("update"))[0];
2777
+ if (!loop) return false;
2778
+ const current = (await tx.select().from(runs).where(and(eq(runs.id, runId), eq(runs.loopId, loop.id), eq(runs.phase, "pending"), eq(runs.requestedBy, "system"), eq(runs.requestedBy, expected.requestedBy), eq(runs.updatedAt, expected.updatedAt))).limit(1).for("update"))[0];
2779
+ if (!current || Date.parse(at) - Date.parse(current.createdAt) <= maxLifetimeMs) return false;
2780
+ if ((await tx.select({ online: machines.online }).from(machines).where(eq(machines.id, current.machineId)).limit(1))[0]?.online) return false;
2781
+ const runIndex = await ensureRunIndexTx(tx, current);
2782
+ return (await tx.update(runs).set({
2783
+ phase: "canceled",
2784
+ runIndex,
2785
+ message,
2786
+ ts: at,
2787
+ updatedAt: at
2788
+ }).where(and(eq(runs.id, runId), eq(runs.phase, "pending"), eq(runs.requestedBy, "system"), eq(runs.updatedAt, expected.updatedAt))).returning({ id: runs.id })).length > 0;
2789
+ });
2790
+ }
2791
+ async function listMachines() {
2792
+ return db.select().from(machines).orderBy(asc(machines.name), asc(machines.id));
2793
+ }
2794
+ async function listMachinesForUser(userId) {
2795
+ return db.select().from(machines).where(eq(machines.userId, userId)).orderBy(asc(machines.name), asc(machines.id));
2796
+ }
2797
+ async function getMachine(id) {
2798
+ return (await db.select().from(machines).where(eq(machines.id, id)))[0];
2799
+ }
2800
+ async function createMachine(input) {
2801
+ return (await db.insert(machines).values({
2802
+ ...input,
2803
+ createdAt: nowIso()
2804
+ }).returning())[0];
2805
+ }
2806
+ async function updateMachine(id, patch) {
2807
+ await db.update(machines).set(patch).where(eq(machines.id, id));
2808
+ return getMachine(id);
2809
+ }
2810
+ /** Atomically retire all loop authority, remove server-owned machine data, revoke
2811
+ * enrollment, and delete the machine. Auth-mode callers pass `ownerUserId` so
2812
+ * ownership is rechecked under the same lock as deletion; open/server callers omit it. */
2813
+ async function forceDeleteMachine(id, ownerUserId) {
2814
+ return db.transaction(async (tx) => {
2815
+ const machine = (await tx.select({
2816
+ id: machines.id,
2817
+ userId: machines.userId
2818
+ }).from(machines).where(eq(machines.id, id)).for("update"))[0];
2819
+ if (!machine) return { state: "not-found" };
2820
+ if (ownerUserId !== void 0 && machine.userId !== ownerUserId) return { state: "forbidden" };
2821
+ const ownedLoops = await tx.select().from(loops).where(eq(loops.machineId, id)).for("update");
2822
+ if (ownerUserId !== void 0 && ownedLoops.some((loop) => loop.userId !== ownerUserId)) return { state: "forbidden" };
2823
+ for (const loop of ownedLoops) if (!await forceDeleteLoopTx(tx, loop.id)) throw new Error(`failed to delete loop ${loop.id}`);
2824
+ await tx.delete(connectKeys).where(eq(connectKeys.machineId, id));
2825
+ await tx.delete(machines).where(eq(machines.id, id));
2826
+ return {
2827
+ state: "deleted",
2828
+ loopIds: ownedLoops.map((loop) => loop.id)
2829
+ };
2830
+ });
2831
+ }
2832
+ async function setMachineOnline(id, online) {
2833
+ await db.update(machines).set({
2834
+ online,
2835
+ lastSeen: nowIso()
2836
+ }).where(eq(machines.id, id));
2837
+ }
2838
+ async function blobExists(hash) {
2839
+ return !!(await db.select({ hash: blobs.hash }).from(blobs).where(eq(blobs.hash, hash)))[0];
2840
+ }
2841
+ /** Verified byte lengths recorded at blob ingress, used to budget reads before
2842
+ * touching the byte store. Missing metadata is intentionally not estimated. */
2843
+ async function blobSizes(hashes) {
2844
+ if (!hashes.length) return /* @__PURE__ */ new Map();
2845
+ const rows = await db.select({
2846
+ hash: blobs.hash,
2847
+ size: blobs.size
2848
+ }).from(blobs).where(inArray(blobs.hash, [...new Set(hashes)]));
2849
+ return new Map(rows.map((row) => [row.hash, row.size]));
2850
+ }
2851
+ async function recordBlob(hash, size) {
2852
+ await db.insert(blobs).values({
2853
+ hash,
2854
+ size,
2855
+ createdAt: nowIso()
2856
+ }).onConflictDoNothing();
2857
+ }
2858
+ /** Does any LIVE artifact_files row on a loop bound to `machineId` point at `hash`?
2859
+ * Gates putBlob: a device may only upload bytes the sync handshake actually asked
2860
+ * it for (a row a prior sync wrote for one of ITS loops), never arbitrary
2861
+ * self-hashed blobs — otherwise any device token is an uncapped blob write channel. */
2862
+ async function machineReferencesBlob(machineId, hash) {
2863
+ return !!(await db.select({ id: artifactFiles.id }).from(artifactFiles).innerJoin(loops, eq(artifactFiles.loopId, loops.id)).where(and(eq(loops.machineId, machineId), eq(artifactFiles.hash, hash), eq(artifactFiles.deleted, false), sql`${loops.artifacts} @> jsonb_build_array(${artifactFiles.path})`)).limit(1))[0];
2864
+ }
2865
+ async function writeConfiguredArtifactTx(tx, input, ts) {
2866
+ await tx.insert(artifactFiles).values({
2867
+ id: randomUUID(),
2868
+ loopId: input.loopId,
2869
+ path: input.path,
2870
+ hash: input.hash,
2871
+ size: input.size,
2872
+ binary: input.binary,
2873
+ oversize: input.oversize,
2874
+ deleted: false,
2875
+ updatedAt: ts
2876
+ }).onConflictDoUpdate({
2877
+ target: [artifactFiles.loopId, artifactFiles.path],
2878
+ set: {
2879
+ hash: input.hash,
2880
+ size: input.size,
2881
+ binary: input.binary,
2882
+ oversize: input.oversize,
2883
+ deleted: false,
2884
+ updatedAt: ts
2885
+ }
2886
+ });
2887
+ }
2888
+ /** Upsert only while the exact literal path remains configured, serialized with
2889
+ * owner edits by the loop row lock. */
2890
+ async function upsertConfiguredArtifactFile(input) {
2891
+ return db.transaction(async (tx) => {
2892
+ const loop = (await tx.select({ artifacts: loops.artifacts }).from(loops).where(eq(loops.id, input.loopId)).for("update"))[0];
2893
+ if (!loop || !loop.artifacts.includes(input.path)) return false;
2894
+ await writeConfiguredArtifactTx(tx, input, nowIso());
2895
+ return true;
2896
+ });
2897
+ }
2898
+ /** Reconcile deletions against both the request's starting allowlist and the
2899
+ * current locked loop config. Newly-added paths are never tombstoned by an older
2900
+ * in-flight manifest; removed paths can never remain live. */
2901
+ async function reconcileConfiguredArtifacts(loopId, configuredAtStart, keepPaths) {
2902
+ return db.transaction(async (tx) => {
2903
+ const loop = (await tx.select({ artifacts: loops.artifacts }).from(loops).where(eq(loops.id, loopId)).for("update"))[0];
2904
+ if (!loop) return 0;
2905
+ const current = new Set(loop.artifacts);
2906
+ const started = new Set(configuredAtStart);
2907
+ const keep = new Set(keepPaths);
2908
+ const stale = (await tx.select().from(artifactFiles).where(and(eq(artifactFiles.loopId, loopId), eq(artifactFiles.deleted, false)))).filter((file) => !current.has(file.path) || started.has(file.path) && !keep.has(file.path));
2909
+ if (!stale.length) return 0;
2910
+ const at = nowIso();
2911
+ await tx.update(artifactFiles).set({
2912
+ hash: null,
2913
+ deleted: true,
2914
+ updatedAt: at
2915
+ }).where(inArray(artifactFiles.id, stale.map((file) => file.id)));
2916
+ return stale.length;
2917
+ });
2918
+ }
2919
+ async function listArtifacts(loopId) {
2920
+ return db.select().from(artifactFiles).where(and(eq(artifactFiles.loopId, loopId), eq(artifactFiles.deleted, false))).orderBy(artifactFiles.path);
2921
+ }
2922
+ async function getArtifactFile(loopId, path) {
2923
+ return (await db.select().from(artifactFiles).where(and(eq(artifactFiles.loopId, loopId), eq(artifactFiles.path, path))))[0];
2924
+ }
2925
+ async function buildLoopManifest(loopId) {
2926
+ const manifest = {};
2927
+ for (const f of await listArtifacts(loopId)) manifest[f.path] = {
2928
+ hash: f.hash,
2929
+ size: f.size,
2930
+ binary: f.binary,
2931
+ oversize: f.oversize
2932
+ };
2933
+ return manifest;
2934
+ }
2935
+ /** Write/overwrite a run's snapshot (path → file metadata). Idempotent on runId
2936
+ * so a re-report of the same run just refreshes the captured artifact state. */
2937
+ async function putRunSnapshot(runId, loopId, manifest) {
2938
+ await db.insert(runSnapshots).values({
2939
+ runId,
2940
+ loopId,
2941
+ manifest,
2942
+ createdAt: nowIso()
2943
+ }).onConflictDoUpdate({
2944
+ target: runSnapshots.runId,
2945
+ set: {
2946
+ loopId,
2947
+ manifest,
2948
+ createdAt: nowIso()
2949
+ }
2950
+ });
2951
+ }
2952
+ async function getRunSnapshot(runId) {
2953
+ return (await db.select().from(runSnapshots).where(eq(runSnapshots.runId, runId)))[0];
2954
+ }
2955
+ /** The most recent indexed snapshot before one run. History order, not a
2956
+ * timestamp tie, is the diff baseline. */
2957
+ async function prevRunSnapshot(loopId, beforeRunIndex) {
2958
+ return (await db.select({ snap: runSnapshots }).from(runSnapshots).innerJoin(runs, eq(runSnapshots.runId, runs.id)).where(and(eq(runSnapshots.loopId, loopId), isNotNull(runs.runIndex), lt(runs.runIndex, beforeRunIndex))).orderBy(desc(runs.runIndex)).limit(1))[0]?.snap;
2959
+ }
2960
+ /**
2961
+ * Prune a loop's run snapshots down to the `keep` most recent (by createdAt),
2962
+ * deleting the rest. Returns the number deleted. This is what makes an old
2963
+ * snapshot's now-unreferenced blobs collectable by the blob GC. `keep <= 0`
2964
+ * means "keep none" (still bounded). Safe to call repeatedly (idempotent once
2965
+ * at/under the window).
2966
+ */
2967
+ async function pruneRunSnapshots(loopId, keep) {
2968
+ const survivors = keep > 0 ? (await db.select({ runId: runSnapshots.runId }).from(runSnapshots).where(eq(runSnapshots.loopId, loopId)).orderBy(desc(runSnapshots.createdAt), desc(runSnapshots.runId)).limit(keep)).map((r) => r.runId) : [];
2969
+ const pred = survivors.length ? and(eq(runSnapshots.loopId, loopId), notInArray(runSnapshots.runId, survivors)) : eq(runSnapshots.loopId, loopId);
2970
+ return (await db.delete(runSnapshots).where(pred).returning({ id: runSnapshots.runId })).length;
2971
+ }
2972
+ async function loopIdsWithSnapshots() {
2973
+ return (await db.selectDistinct({ loopId: runSnapshots.loopId }).from(runSnapshots)).map((r) => r.loopId);
2974
+ }
2975
+ /**
2976
+ * The full set of blob hashes still referenced by a LIVE row — the GC's keep
2977
+ * set. A hash is live if ANY artifact_files row points at it (deleted tombstones
2978
+ * carry hash=null, so they don't pin a blob) OR ANY retained run_snapshot's
2979
+ * manifest references it. Computed in one pass so the GC never deletes a blob a
2980
+ * snapshot still needs for its diff.
2981
+ */
2982
+ async function liveBlobRefs() {
2983
+ const refs = /* @__PURE__ */ new Set();
2984
+ for (const r of await db.selectDistinct({ hash: artifactFiles.hash }).from(artifactFiles).where(isNotNull(artifactFiles.hash))) if (r.hash) refs.add(r.hash);
2985
+ for (const r of await db.select({ manifest: runSnapshots.manifest }).from(runSnapshots)) for (const entry of Object.values(r.manifest)) if (entry?.hash) refs.add(entry.hash);
2986
+ return refs;
2987
+ }
2988
+ /** Blob hashes whose metadata row predates `cutoffIso` (GC candidates — the grace
2989
+ * window excludes freshly-written blobs a concurrent sync may be referencing). */
2990
+ async function blobHashesOlderThan(cutoffIso) {
2991
+ return (await db.select({ hash: blobs.hash }).from(blobs).where(lt(blobs.createdAt, cutoffIso))).map((r) => r.hash);
2992
+ }
2993
+ /** Delete a blob's metadata row (the bytes are reclaimed separately via the
2994
+ * BlobStore). Idempotent. */
2995
+ async function deleteBlob(hash) {
2996
+ await db.delete(blobs).where(eq(blobs.hash, hash));
2997
+ }
2998
+ /** Indexed point check: does any LIVE artifact_files row still point at this hash?
2999
+ * The GC's cheap, always-fresh per-candidate guard (the common re-reference path) —
3000
+ * uses the artifact_files_hash index, so it stays O(1) even as candidates pile up. */
3001
+ async function artifactFileReferencesHash(hash) {
3002
+ return !!(await db.select({ id: artifactFiles.id }).from(artifactFiles).where(eq(artifactFiles.hash, hash)))[0];
3003
+ }
3004
+ /** Every blob hash referenced by ANY retained run_snapshot's manifest — the full
3005
+ * snapshot scan deserialized ONCE into a Set so the GC can answer per-candidate
3006
+ * snapshot membership in O(1) instead of re-scanning the whole table per garbage
3007
+ * hash. The GC rebuilds this only when the snapshot row count changes (a report()
3008
+ * raced the pass), so a snapshot that comes to reference a hash mid-pass is still
3009
+ * caught — closing the GC-check-time gap where a snapshot references a hash no live
3010
+ * file row does — without paying O(garbage × snapshots). */
3011
+ async function snapshotBlobRefs() {
3012
+ const refs = /* @__PURE__ */ new Set();
3013
+ for (const r of await db.select({ manifest: runSnapshots.manifest }).from(runSnapshots)) for (const entry of Object.values(r.manifest)) if (entry?.hash) refs.add(entry.hash);
3014
+ return refs;
3015
+ }
3016
+ /** Count of retained run_snapshot rows — the GC's cheap change-detector for deciding
3017
+ * whether to rebuild its precomputed snapshotBlobRefs() set mid-pass. */
3018
+ async function countRunSnapshots() {
3019
+ const r = (await db.select({ n: sql`count(*)` }).from(runSnapshots))[0];
3020
+ return Number(r?.n ?? 0);
3021
+ }
3022
+ /** Per-file byte cap. At or under ⇒ bytes sync; over ⇒ metadata-only (no bytes). */
3023
+ var BLOB_CAP = 10 * 1024 * 1024;
3024
+ /** Hard ceiling on the exact-manifest sync POST body. */
3025
+ var SYNC_BODY_CAP = 32 * 1024 * 1024;
3026
+ var SYNC_FIELDS = new Set(["loopId", "manifest"]);
3027
+ var MANIFEST_FIELDS = new Set([
3028
+ "path",
3029
+ "hash",
3030
+ "size",
3031
+ "binary",
3032
+ "oversize"
3033
+ ]);
3034
+ var MAX_STORED_SIZE = 2147483647;
3035
+ function exactRecord(value, fields) {
3036
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
3037
+ const proto = Object.getPrototypeOf(value);
3038
+ if (proto !== Object.prototype && proto !== null) return false;
3039
+ const keys = Object.keys(value);
3040
+ return keys.length === fields.size && keys.every((key) => fields.has(key));
3041
+ }
3042
+ function sha256Buf(bytes) {
3043
+ return createHash("sha256").update(bytes).digest("hex");
3044
+ }
3045
+ function isValidHash(hash) {
3046
+ return typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash);
3047
+ }
3048
+ /**
3049
+ * Validate and preserve an untrusted, loop-folder-relative literal path. Returns
3050
+ * null if it is absolute, contains an empty/dot/traversal segment, is empty, or
3051
+ * carries a NUL (no real filesystem produces one, and Postgres rejects it).
3052
+ */
3053
+ function safeRelPath(raw) {
3054
+ if (typeof raw !== "string" || raw.length === 0 || raw.includes("\0")) return null;
3055
+ if (raw.startsWith("/") || raw.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(raw)) return null;
3056
+ if (raw.split(/[\\/]/).some((part) => part === "" || part === "." || part === "..")) return null;
3057
+ return raw;
3058
+ }
3059
+ /** Parse the one canonical sync envelope without coercion or omitted-field defaults. */
3060
+ function parseArtifactSyncBody(value) {
3061
+ if (!exactRecord(value, SYNC_FIELDS)) return null;
3062
+ const { loopId, manifest } = value;
3063
+ if (typeof loopId !== "string" || loopId.length === 0 || loopId.length > 200 || loopId.includes("\0") || !Array.isArray(manifest)) return null;
3064
+ const entries = [];
3065
+ const paths = /* @__PURE__ */ new Set();
3066
+ for (const raw of manifest) {
3067
+ if (!exactRecord(raw, MANIFEST_FIELDS)) return null;
3068
+ const { path, hash, size, binary, oversize } = raw;
3069
+ const rel = safeRelPath(path);
3070
+ if (rel === null || paths.has(rel) || typeof size !== "number" || !Number.isSafeInteger(size) || size < 0 || size > MAX_STORED_SIZE || typeof binary !== "boolean" || typeof oversize !== "boolean") return null;
3071
+ if (oversize) {
3072
+ if (size <= 10485760 || hash !== null) return null;
3073
+ entries.push({
3074
+ path: rel,
3075
+ hash: null,
3076
+ size,
3077
+ binary,
3078
+ oversize: true
3079
+ });
3080
+ } else {
3081
+ if (size > 10485760 || !isValidHash(hash)) return null;
3082
+ entries.push({
3083
+ path: rel,
3084
+ hash,
3085
+ size,
3086
+ binary,
3087
+ oversize: false
3088
+ });
3089
+ }
3090
+ paths.add(rel);
3091
+ }
3092
+ return {
3093
+ loopId,
3094
+ manifest: entries
3095
+ };
3096
+ }
3097
+ /**
3098
+ * Machine + run credential helpers. Device tokens (`dk_…`) identify a machine
3099
+ * (its id is derived from the token: `m-sha256(token)[:16]`). A run
3100
+ * LEASE (`rk_…`) is minted per delivery, bound to one run, and carries the
3101
+ * run's least-privilege caps — the CLI dispatch authorizes the `pievo` shim
3102
+ * against it. Its lifecycle is a small state machine (`active` →
3103
+ * `terminal-grace` → `reconciliation-only`/`retired`), not a mint→revoke pair;
3104
+ * see `RunLease` below.
3105
+ *
3106
+ * Leases and connect-key bindings are DURABLE (run_leases / connect_keys
3107
+ * tables): they must survive a deploy, or every restart 401s the in-flight
3108
+ * runs' callbacks/finalize and prevents a freshly connected machine from
3109
+ * enrolling under its owner. The 15-minute `new` idempotency window stays in-process;
3110
+ * losing it only degrades retry deduplication, never persisted data.
3111
+ */
3112
+ function sha256(s) {
3113
+ return createHash("sha256").update(s).digest("hex");
3114
+ }
3115
+ function mintDeviceToken() {
3116
+ return `dk_${randomBytes(15).toString("hex")}`;
3117
+ }
3118
+ function machineIdFromToken(token) {
3119
+ return `m-${sha256(token).slice(0, 16)}`;
3120
+ }
3121
+ /**
3122
+ * Whether a string has the one current device-token wire shape. This malformed-input
3123
+ * filter runs before enrollment lookup; the connect-key gate remains the auth boundary.
3124
+ */
3125
+ var DEVICE_TOKEN_RE = /^dk_[0-9a-f]{30}$/;
3126
+ function isDeviceTokenShape(token) {
3127
+ return DEVICE_TOKEN_RE.test(token);
3128
+ }
3129
+ /** Keep bindings long enough for a leisurely paste, then drop (bounded table). */
3130
+ var CONNECT_KEY_TTL_MS = 1440 * 60 * 1e3;
3131
+ function connectKeyFresh(mintedAt, now) {
3132
+ return now - Date.parse(mintedAt) <= CONNECT_KEY_TTL_MS;
3133
+ }
3134
+ /** Bind a freshly minted connect key to its owner and prune expired rows. */
3135
+ async function rememberConnectKey(connectKey, binding) {
3136
+ const now = /* @__PURE__ */ new Date();
3137
+ await db.delete(connectKeys).where(lt(connectKeys.mintedAt, (/* @__PURE__ */ new Date(now.getTime() - CONNECT_KEY_TTL_MS)).toISOString()));
3138
+ const row = {
3139
+ machineId: machineIdFromToken(connectKey),
3140
+ userId: binding.userId,
3141
+ mintedAt: now.toISOString()
3142
+ };
3143
+ await db.insert(connectKeys).values(row).onConflictDoUpdate({
3144
+ target: connectKeys.machineId,
3145
+ set: row
3146
+ });
3147
+ }
3148
+ async function readConnectKeyBinding(connectKey, now = Date.now()) {
3149
+ if (!connectKey) return void 0;
3150
+ const row = (await db.select().from(connectKeys).where(eq(connectKeys.machineId, machineIdFromToken(connectKey))))[0];
3151
+ if (!row || !connectKeyFresh(row.mintedAt, now)) return void 0;
3152
+ return { userId: row.userId };
3153
+ }
3154
+ /** How long a terminal-grace lease stays alive to accept one late wake-report.
3155
+ * A laptop can sleep overnight or across a weekend before the daemon resumes. */
3156
+ var TERMINAL_GRACE_MS = 1440 * 60 * 1e3;
3157
+ /** Leases live in the `run_leases` table, keyed by sha256(full `rk_…` wire
3158
+ * token), so a deploy is invisible to an in-flight run and a DB leak never hands
3159
+ * out live credentials. In rows, `expiresAt` null encodes `Infinity`. */
3160
+ function leaseFromRow(row) {
3161
+ return {
3162
+ runId: row.runId,
3163
+ loopId: row.loopId,
3164
+ machineId: row.machineId,
3165
+ state: row.state,
3166
+ expiresAt: row.expiresAt == null ? Number.POSITIVE_INFINITY : Date.parse(row.expiresAt)
3167
+ };
3168
+ }
3169
+ /** Mint a fresh run lease and return its wire token (`rk_…`, so the unified CLI
3170
+ * dispatch can tell a run credential from a device `dk_…` in O(1) before any
3171
+ * lookup). Starts `active` with no expiry. */
3172
+ var RUN_TOKEN_RE = /^rk_[0-9a-f]{32}$/;
3173
+ function isRunTokenShape(token) {
3174
+ return RUN_TOKEN_RE.test(token);
3175
+ }
3176
+ /** Resolve a run lease. An elapsed reconciliation window is atomically reduced
3177
+ * to durable, non-authorizing retired evidence rather than deleted. */
3178
+ async function resolveLease(token, now = Date.now()) {
3179
+ if (!isRunTokenShape(token)) return void 0;
3180
+ const tokenHash = sha256(token);
3181
+ let row = (await db.select().from(runLeases).where(eq(runLeases.tokenHash, tokenHash)))[0];
3182
+ if (!row) return void 0;
3183
+ if ((row.state === "terminal-grace" || row.state === "reconciliation-only") && row.expiresAt != null && now > Date.parse(row.expiresAt)) {
3184
+ row = (await db.update(runLeases).set({
3185
+ state: "retired",
3186
+ expiresAt: null
3187
+ }).where(and(eq(runLeases.tokenHash, tokenHash), inArray(runLeases.state, ["terminal-grace", "reconciliation-only"]), isNotNull(runLeases.expiresAt), lt(runLeases.expiresAt, new Date(now).toISOString()))).returning())[0] ?? (await db.select().from(runLeases).where(eq(runLeases.tokenHash, tokenHash)))[0];
3188
+ if (!row) return void 0;
3189
+ }
3190
+ return leaseFromRow(row);
3191
+ }
3192
+ /** Retire a lease outside a terminal write transaction (canceled/losing report
3193
+ * cleanup and expiry paths). Normal finalize and reclaimed-run
3194
+ * reconcile consume the row inside their store transaction so run+loop+lease are
3195
+ * one single-shot commit. */
3196
+ async function retireLease(token) {
3197
+ await db.delete(runLeases).where(eq(runLeases.tokenHash, sha256(token)));
3198
+ }
3199
+ /** State-aware lease maintenance. Expired reconciliation authority becomes a
3200
+ * durable retired tombstone. Active and already-retired rows are never age-pruned. */
3201
+ async function pruneExpiredLeases(now = Date.now()) {
3202
+ await db.update(runLeases).set({
3203
+ state: "retired",
3204
+ expiresAt: null
3205
+ }).where(and(inArray(runLeases.state, ["terminal-grace", "reconciliation-only"]), isNotNull(runLeases.expiresAt), lt(runLeases.expiresAt, new Date(now).toISOString())));
3206
+ }
3207
+ var newIdempotency = /* @__PURE__ */ new Map();
3208
+ function pruneNewIdempotency(now) {
3209
+ for (const [key, rec] of newIdempotency) if (now - rec.at > 9e5) newIdempotency.delete(key);
3210
+ }
3211
+ function recordNewIdempotency(key, machineId, loopId, now = Date.now()) {
3212
+ pruneNewIdempotency(now);
3213
+ newIdempotency.set(key, {
3214
+ loopId,
3215
+ machineId,
3216
+ at: now
3217
+ });
3218
+ }
3219
+ /** The loop a still-live key already created for THIS machine, or undefined (a miss,
3220
+ * an expired key — dropped here — or a cross-machine record). NON-evicting on a hit:
3221
+ * a genuine retry may arrive more than once within the window. */
3222
+ function readNewIdempotency(key, machineId, now = Date.now()) {
3223
+ const rec = newIdempotency.get(key);
3224
+ if (!rec) return void 0;
3225
+ if (now - rec.at > 9e5) {
3226
+ newIdempotency.delete(key);
3227
+ return;
3228
+ }
3229
+ if (rec.machineId !== machineId) return void 0;
3230
+ return rec.loopId;
3231
+ }
3232
+ /**
3233
+ * Derive a machine's presence from its stored `online` flag + `lastSeen` stamp.
3234
+ * `online` requires BOTH the flag and a fresh stamp (the stored flag lags the
3235
+ * sweep by up to one interval); everything else keys off how stale `lastSeen` is.
3236
+ */
3237
+ function machinePresence(online, lastSeen, now = Date.now()) {
3238
+ const seenAt = lastSeen ? Date.parse(lastSeen) : NaN;
3239
+ if (!Number.isFinite(seenAt)) return "offline";
3240
+ const age = now - seenAt;
3241
+ if (!!online && age < 3e4) return "online";
3242
+ if (age < 216e5) return "asleep";
3243
+ return "offline";
3244
+ }
3245
+ /**
3246
+ * Tiny semver comparison — just enough to decide "is the daemon older than the
3247
+ * latest published version?" for the web's update hint. No dependency; we only
3248
+ * need numeric-core ordering (major.minor.patch), and a pre-release is treated
3249
+ * as older than its release (a "0.9.0-rc.1" daemon is still behind "0.9.0").
3250
+ */
3251
+ /** Strict SemVer validation for security/protocol gates. Unlike the update-hint
3252
+ * comparison below, this accepts no whitespace, `v` prefix, partial core, or
3253
+ * trailing garbage. */
3254
+ function isValidSemver(value) {
3255
+ const plus = value.split("+");
3256
+ if (plus.length > 2) return false;
3257
+ const [versionAndPre, build] = plus;
3258
+ if (build !== void 0 && !validIdentifiers(build, false)) return false;
3259
+ const dash = versionAndPre.indexOf("-");
3260
+ const numeric = dash < 0 ? versionAndPre : versionAndPre.slice(0, dash);
3261
+ const pre = dash < 0 ? void 0 : versionAndPre.slice(dash + 1);
3262
+ if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(numeric)) return false;
3263
+ return pre === void 0 || validIdentifiers(pre, true);
3264
+ }
3265
+ function validIdentifiers(value, rejectNumericLeadingZero) {
3266
+ if (!value) return false;
3267
+ return value.split(".").every((identifier) => {
3268
+ if (!/^[0-9A-Za-z-]+$/.test(identifier)) return false;
3269
+ return !rejectNumericLeadingZero || !/^\d+$/.test(identifier) || identifier === "0" || !identifier.startsWith("0");
3270
+ });
3271
+ }
3272
+ function core(v) {
3273
+ const m = /^\s*v?(\d+)\.(\d+)\.(\d+)/.exec(v);
3274
+ if (!m) return null;
3275
+ return [
3276
+ Number(m[1]),
3277
+ Number(m[2]),
3278
+ Number(m[3])
3279
+ ];
3280
+ }
3281
+ function isPre(v) {
3282
+ return /^\s*v?\d+\.\d+\.\d+-/.test(v);
3283
+ }
3284
+ /**
3285
+ * `true` when `current` is strictly older than `latest`. Returns `false` on any
3286
+ * unparseable/equal/newer input — the hint is opt-in, so we only ever show it
3287
+ * when we can be confident the daemon is genuinely behind.
3288
+ */
3289
+ function isOutdated(current, latest) {
3290
+ if (!current || !latest) return false;
3291
+ const a = core(current);
3292
+ const b = core(latest);
3293
+ if (!a || !b) return false;
3294
+ for (let i = 0; i < 3; i++) {
3295
+ const ai = a[i];
3296
+ const bi = b[i];
3297
+ if (ai < bi) return true;
3298
+ if (ai > bi) return false;
3299
+ }
3300
+ return isPre(current) && !isPre(latest);
3301
+ }
3302
+ var reserved = new Set(RESERVED_LOOP_TAGS);
3303
+ function normalizeLoopTag(value) {
3304
+ return value.normalize("NFKC").trim().replace(/\s+/gu, " ").toLocaleLowerCase("en-US");
3305
+ }
3306
+ function validateLoopTags(value) {
3307
+ if (value === void 0) return {
3308
+ ok: true,
3309
+ value: []
3310
+ };
3311
+ if (!Array.isArray(value)) return {
3312
+ ok: false,
3313
+ detail: "tags must be an array of strings"
3314
+ };
3315
+ if (value.length > 4) return {
3316
+ ok: false,
3317
+ detail: `tags must contain at most 4 values`
3318
+ };
3319
+ const tags = [];
3320
+ const seen = /* @__PURE__ */ new Set();
3321
+ for (const entry of value) {
3322
+ if (typeof entry !== "string") return {
3323
+ ok: false,
3324
+ detail: "tags must contain only strings"
3325
+ };
3326
+ if (/\p{Cc}|\p{Cf}|\p{Default_Ignorable_Code_Point}/u.test(entry)) return {
3327
+ ok: false,
3328
+ detail: "tags must not contain control or invisible characters"
3329
+ };
3330
+ const tag = normalizeLoopTag(entry);
3331
+ if (!tag) return {
3332
+ ok: false,
3333
+ detail: "tags must not contain empty values"
3334
+ };
3335
+ if ([...tag].length > 64) return {
3336
+ ok: false,
3337
+ detail: `each tag must contain at most 64 characters`
3338
+ };
3339
+ if (reserved.has(tag)) return {
3340
+ ok: false,
3341
+ detail: `reserved tag: ${tag}`
3342
+ };
3343
+ if (seen.has(tag)) return {
3344
+ ok: false,
3345
+ detail: `duplicate tag: ${tag}`
3346
+ };
3347
+ seen.add(tag);
3348
+ tags.push(tag);
3349
+ }
3350
+ return {
3351
+ ok: true,
3352
+ value: [...tags].sort()
3353
+ };
3354
+ }
3355
+ //#endregion
3356
+ export { getLoop as $, reclaimRun as $t, createLoop as A, updateLoop as An, logger as At, enqueueRun as B, parseArtifactSyncBody as Bt, claimReadyRunForMachine as C, sha256Buf as Cn, listLoops as Ct, countRuns as D, stopLoop as Dn, listRuns as Dt, countRunSnapshots as E, startLoop as En, listMachinesForUser as Et, dbWatchdogFailureThreshold as F, machinePresence as Ft, forceDeleteMachine as G, pruneRunSnapshots as Gt, expirePendingRun as H, pauseLoopState as Ht, dbWatchdogIntervalMs as I, machineReferencesBlob as It, getBaseURL as J, putTerminalReportIncidentIfAbsent as Jt, gcIntervalMs as K, putReportReceiptIfAbsent as Kt, dbWatchdogTimeoutMs as L, mintDeviceToken as Lt, dataDir as M, upsertConfiguredArtifactFile as Mn, loopIdsWithSnapshots as Mt, db as N, validateLoopTags as Nn, loopsForMachine as Nt, createDefu as O, toKebabCase as On, listRunsBefore as Ot, dbWatchdogEnabled as P, wildcardMatch as Pn, machineIdFromToken as Pt, getHost as Q, recentLoopUsage as Qt, defu as R, normalizePathname as Rt, capitalizeFirstLetter as S, sha256 as Sn, listArtifacts as St, coerceCodingAgent as T, snapshotRetention as Tn, listMachines as Tt, finalizeRunningRun as U, prevRunSnapshot as Ut, env as V, pauseLoop as Vt, forceDeleteLoop as W, pruneExpiredLeases as Wt, getEnvVar as X, readConnectKeyBinding as Xt, getBooleanEnvVar as Y, r2Config as Yt, getExactTerminalReportIncident as Z, readNewIdempotency as Zt, blobExists as _, runningRunForLoop as _n, isTest as _t, BetterAuthError as a, recordRunReportOnce as an, getRunSnapshot as at, blobSizes as b, safeRelPath as bn, kAPIErrorHeaderSymbol as bt, ENV as c, releaseAbsentReconciliations as cn, hasRunningRun as ct, ValidationError$1 as d, requestDeleteLoop as dn, isDeviceTokenShape as dt, reconcileConfiguredArtifacts as en, getMachine as et, acknowledgeRetiredReport as f, requestRunCancel as fn, isDynamicBaseURLConfig as ft, betterFetch as g, runMigrations as gn, isSafeUrlScheme as gt, artifactFileReferencesHash as h, retireLease as hn, isRequestLike as ht, BLOB_CAP as i, recordNewIdempotency as in, getRun as it, createMachine as j, updateMachine as jn, loginGateEnabled as jt, createFetch as k, tryDeleteLoop as kn, liveBlobRefs as kt, SYNC_BODY_CAP as l, rememberConnectKey as ln, initializeCronCadence as lt, advanceDueSchedules as m, resolveLease as mn, isProduction as mt, APIError$1 as n, reconciliationStatesForRuns as nn, getProtocol as nt, BetterCallError as o, refreshRunHeartbeats as on, getTerminalReportIncidents as ot, acknowledgeRetiredTerminalIncident as p, resolveBaseURL as pn, isOutdated as pt, getArtifactFile as q, putRunSnapshot as qt, BASE_ERROR_CODES as r, recordBlob as rn, getReportReceipt as rt, CODING_AGENTS as s, rejectTerminalReport as sn, hasPendingRun as st, APIError as t, reconcileReclaimedRun as tn, getOrigin as tt, TERMINAL_GRACE_MS as u, repairTerminalRunLeases as un, isDevelopment as ut, blobGcGraceMs as v, runningRunsForMachine as vn, isValidHash as vt, closeClient as w, snapshotBlobRefs as wn, listLoopsForUser as wt, buildLoopManifest as x, setMachineOnline as xn, lastRun as xt, blobHashesOlderThan as y, runs as yn, isValidSemver as yt, deleteBlob as z, openRuns as zt };