@kky42/pievo-server 2.4.13 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{actionUi-D8m1FDHO.js → actionUi-BZrLpgHT.js} +1 -1
  3. package/.output/public/assets/app-DXLuQNb9.css +2 -0
  4. package/.output/public/assets/index-Di_wpAtL.js +10 -0
  5. package/.output/public/assets/loops._loopId-X3ClNPMY.js +67 -0
  6. package/.output/public/assets/loops._loopId_.runs._runId-ByU2BSyF.js +3 -0
  7. package/.output/public/assets/preload-helper-hRTYB_ju.js +3 -0
  8. package/.output/public/assets/{routes-79D939J4.js → routes-BIOJZsig.js} +1 -1
  9. package/.output/public/assets/routes-K8D5uBiV.js +1 -0
  10. package/.output/server/_chunks/ssr-renderer.mjs +1 -1
  11. package/.output/server/_libs/{drizzle-orm+postgres.mjs → drizzle-orm.mjs} +93 -2218
  12. package/.output/server/_ssr/{MachinesModal-DBIP648Z.mjs → MachinesModal-CmVMec6B.mjs} +52 -46
  13. package/.output/server/_ssr/{actionUi-C_WswNb1.mjs → actionUi-DE-Pb2tv.mjs} +4 -5
  14. package/.output/server/_ssr/{artifactFiles-BvVmixrs.mjs → artifactFiles-Di1xDKvX.mjs} +5 -7
  15. package/.output/server/_ssr/{artifactKind-DRCbYHZU.mjs → artifactKind-DqpmtxDU.mjs} +3 -9
  16. package/.output/server/_ssr/{boot-U-CFI65B.mjs → boot-CYRi-wez.mjs} +531 -1976
  17. package/.output/server/_ssr/lifecycleUi-BsbUsi5K.mjs +106 -0
  18. package/.output/server/_ssr/{loopApi-C4nN0msL.mjs → loopApi-Bez1rHV0.mjs} +50 -65
  19. package/.output/server/_ssr/{loopProjection-DMSNHux6.mjs → loopProjection-esCjyZJg.mjs} +4 -20
  20. package/.output/server/_ssr/{loops._loopId-Q4pRYPip.mjs → loops._loopId-KUezNY69.mjs} +133 -24
  21. package/.output/server/_ssr/{loops._loopId_.runs._runId-DNXVmCt5.mjs → loops._loopId_.runs._runId-BFP9oMrp.mjs} +2 -7
  22. package/.output/server/_ssr/{loops._loopId_.runs._runId-ty3AhkOi.mjs → loops._loopId_.runs._runId-DOkUEesG.mjs} +18 -33
  23. package/.output/server/_ssr/{machineFns-CkNms3zA.mjs → machineFns-CniGcY8s.mjs} +13 -49
  24. package/.output/server/_ssr/networkAccess-CIlK3ION.mjs +30 -0
  25. package/.output/server/_ssr/router-BFIH2UAE.mjs +362 -0
  26. package/.output/server/_ssr/routes-BSVBANI3.mjs +12 -0
  27. package/.output/server/_ssr/{routes-BaanDO41.mjs → routes-DnE7XDe_.mjs} +242 -77
  28. package/.output/server/_ssr/{routes-B3fulW5I.mjs → routes-ngpX7h8D.mjs} +2 -4
  29. package/.output/server/_ssr/ssr.mjs +25 -34
  30. package/.output/server/_ssr/start-B5_9Z5W-.mjs +35 -0
  31. package/.output/server/{_tanstack-start-manifest_v-DkmxsHDK.mjs → _tanstack-start-manifest_v-DnwAupbZ.mjs} +8 -13
  32. package/.output/server/drizzle/0000_baseline.sql +6 -97
  33. package/.output/server/drizzle/meta/_journal.json +2 -16
  34. package/.output/server/index.mjs +43 -50
  35. package/drizzle/0000_baseline.sql +6 -97
  36. package/drizzle/meta/_journal.json +2 -16
  37. package/package.json +4 -8
  38. package/scripts/pievo-server.mjs +49 -47
  39. package/scripts/server-cli-lib.mjs +21 -12
  40. package/scripts/server-network.mjs +139 -0
  41. package/.output/public/assets/SignIn-CDCdDD1a.js +0 -1
  42. package/.output/public/assets/app-PPhgN9iN.css +0 -2
  43. package/.output/public/assets/index-c0XPWXuz.js +0 -10
  44. package/.output/public/assets/loops._loopId-D7i_BGiE.js +0 -67
  45. package/.output/public/assets/loops._loopId_.runs._runId-CksSrEY7.js +0 -3
  46. package/.output/public/assets/preload-helper-Zcmr31XG.js +0 -5
  47. package/.output/public/assets/routes-BYgl3Tmv.js +0 -1
  48. package/.output/server/_libs/@aws-crypto/crc32+[...].mjs +0 -419
  49. package/.output/server/_libs/@aws-sdk/checksums+[...].mjs +0 -10732
  50. package/.output/server/_libs/@aws-sdk/client-s3+[...].mjs +0 -9475
  51. package/.output/server/_libs/@aws-sdk/credential-provider-http+[...].mjs +0 -137
  52. package/.output/server/_libs/@aws-sdk/credential-provider-ini+[...].mjs +0 -440
  53. package/.output/server/_libs/@aws-sdk/credential-provider-process+[...].mjs +0 -62
  54. package/.output/server/_libs/@aws-sdk/credential-provider-sso+[...].mjs +0 -818
  55. package/.output/server/_libs/@aws-sdk/credential-provider-web-identity+[...].mjs +0 -59
  56. package/.output/server/_libs/@smithy/credential-provider-imds+[...].mjs +0 -342
  57. package/.output/server/_libs/aws-crypto__crc32c.mjs +0 -314
  58. package/.output/server/_libs/aws-sdk__nested-clients.mjs +0 -2671
  59. package/.output/server/_ssr/SignIn-Bp3uWaCQ.mjs +0 -53
  60. package/.output/server/_ssr/artifacts-CzDJLaa6.mjs +0 -2
  61. package/.output/server/_ssr/auth-BV0MxVdo.mjs +0 -38355
  62. package/.output/server/_ssr/auth-client-RTpMpT4L.mjs +0 -1070
  63. package/.output/server/_ssr/boot-CsLXrLEa.mjs +0 -2
  64. package/.output/server/_ssr/bun-sqlite-dialect-BW9W1_Ps-D9KUBJ-L.mjs +0 -162
  65. package/.output/server/_ssr/core-RrmWoKne.mjs +0 -5
  66. package/.output/server/_ssr/d1-sqlite-dialect-BLC8LXE6-CVHRzH1n.mjs +0 -112
  67. package/.output/server/_ssr/db-BTX-xORB.mjs +0 -2
  68. package/.output/server/_ssr/dist-D7UDVZ5G.mjs +0 -342
  69. package/.output/server/_ssr/factory-DYFx1fg-.mjs +0 -6903
  70. package/.output/server/_ssr/kysely-adapter-CwdB7yBh.mjs +0 -2
  71. package/.output/server/_ssr/kysely-migration-tables-JkVUjPF_-bPAX9LR_.mjs +0 -19
  72. package/.output/server/_ssr/logger-CwiKSMp6.mjs +0 -2
  73. package/.output/server/_ssr/node-sqlite-dialect-B_g92oFK.mjs +0 -162
  74. package/.output/server/_ssr/router-DCNZ2xLm.mjs +0 -543
  75. package/.output/server/_ssr/routes-8JSF7bX_.mjs +0 -27
  76. package/.output/server/_ssr/runDiff-D7CxAgYL.mjs +0 -2
  77. package/.output/server/_ssr/start-4ifArtKx.mjs +0 -4
  78. package/.output/server/_ssr/store-DSNbXxqz.mjs +0 -2
  79. package/.output/server/_ssr/tokens-DzUv7CCp.mjs +0 -2
  80. package/.output/server/drizzle/0001_add_pi_agent.sql +0 -9
  81. package/.output/server/drizzle/0002_remove_teams.sql +0 -15
  82. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  83. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  84. package/.output/server/node_modules/tslib/package.json +0 -47
  85. package/.output/server/node_modules/tslib/tslib.js +0 -484
  86. package/.output/server/package.json +0 -9
  87. package/drizzle/0001_add_pi_agent.sql +0 -9
  88. package/drizzle/0002_remove_teams.sql +0 -15
  89. package/scripts/prestart.mjs +0 -66
@@ -1,9 +1,9 @@
1
- import { o as __toESM } from "../_runtime.mjs";
1
+ import { o as __toESM, r as __exportAll$1 } from "../_runtime.mjs";
2
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, m as gte, 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, x as lte } from "../_libs/drizzle-orm+postgres.mjs";
3
+ import { C as integer, S as jsonb, T as sql, _ as notInArray, a as asc, b as timestamp, c as eq, d as inArray, f as isNotNull, g as ne, h as lte, i as check, l as gt, m as lt, n as index, o as desc, p as isNull, r as uniqueIndex, s as and, t as drizzle, u as gte, v as or, w as boolean, x as text, y as pgTable } from "../_libs/drizzle-orm.mjs";
4
+ import { t as E } from "../_libs/croner.mjs";
4
5
  import { t as require_pino } from "../_libs/pino+[...].mjs";
5
6
  import { t as require_pino_pretty } from "../_libs/pino-pretty+[...].mjs";
6
- import { t as E } from "../_libs/croner.mjs";
7
7
  import { t as createTwoFilesPatch } from "../_libs/diff.mjs";
8
8
  import { createHash, randomBytes, randomUUID } from "node:crypto";
9
9
  import fs from "node:fs";
@@ -11,1394 +11,158 @@ import { fileURLToPath } from "node:url";
11
11
  import path from "node:path";
12
12
  import os from "node:os";
13
13
  import fs$1 from "node:fs/promises";
14
- //#region node_modules/.nitro/vite/services/ssr/assets/boot-U-CFI65B.js
14
+ //#region node_modules/.nitro/vite/services/ssr/assets/loopTags-C6vATFCp.js
15
+ var loopTags_C6vATFCp_exports = /* @__PURE__ */ __exportAll$1({
16
+ $: () => lastRun,
17
+ A: () => buildLoopManifest,
18
+ At: () => runningRunForLoop,
19
+ B: () => forceDeleteLoop,
20
+ Bt: () => upsertConfiguredArtifactFile,
21
+ C: () => acknowledgeRetiredReport,
22
+ Ct: () => recordRunReportOnce,
23
+ D: () => blobExists,
24
+ Dt: () => repairTerminalRunLeases,
25
+ E: () => artifactFileReferencesHash,
26
+ Et: () => releaseAbsentReconciliations,
27
+ F: () => createMachine,
28
+ Ft: () => stopLoop,
29
+ G: () => getMachine,
30
+ Gt: () => CODING_AGENTS,
31
+ H: () => getArtifactFile,
32
+ Ht: () => db,
33
+ I: () => deleteBlob,
34
+ It: () => store_exports,
35
+ J: () => getRunSnapshot,
36
+ Jt: () => logger_exports,
37
+ K: () => getReportReceipt,
38
+ Kt: () => coerceCodingAgent,
39
+ L: () => enqueueRun,
40
+ Lt: () => tryDeleteLoop,
41
+ M: () => countRunSnapshots,
42
+ Mt: () => setMachineOnline,
43
+ N: () => countRuns,
44
+ Nt: () => snapshotBlobRefs,
45
+ O: () => blobHashesOlderThan,
46
+ Ot: () => requestDeleteLoop,
47
+ P: () => createLoop,
48
+ Pt: () => startLoop,
49
+ Q: () => initializeCronCadence,
50
+ Qt: () => __exportAll,
51
+ R: () => expirePendingRun,
52
+ Rt: () => updateLoop,
53
+ S: () => sha256Buf,
54
+ St: () => recordBlob,
55
+ T: () => advanceDueSchedules,
56
+ Tt: () => rejectTerminalReport,
57
+ U: () => getExactTerminalReportIncident,
58
+ Ut: () => runMigrations,
59
+ V: () => forceDeleteMachine,
60
+ Vt: () => closeClient,
61
+ W: () => getLoop,
62
+ Wt: () => runs,
63
+ X: () => hasPendingRun,
64
+ Xt: () => GC_INTERVAL_MS,
65
+ Y: () => getTerminalReportIncidents,
66
+ Yt: () => BLOB_GC_GRACE_MS,
67
+ Z: () => hasRunningRun,
68
+ Zt: () => dataDir,
69
+ _: () => BLOB_CAP,
70
+ _t: () => recentLoopUsage,
71
+ a: () => TERMINAL_GRACE_MS,
72
+ at: () => liveBlobRefs,
73
+ b: () => parseArtifactSyncBody,
74
+ bt: () => reconcileReclaimedRun,
75
+ c: () => machineIdFromToken,
76
+ ct: () => machineReferencesBlob,
77
+ d: () => readNewIdempotency,
78
+ dt: () => pauseLoopState,
79
+ et: () => listArtifacts,
80
+ f: () => recordNewIdempotency,
81
+ ft: () => prevRunSnapshot,
82
+ g: () => tokens_exports,
83
+ gt: () => putTerminalReportIncidentIfAbsent,
84
+ h: () => sha256,
85
+ ht: () => putRunSnapshot,
86
+ i: () => machinePresence,
87
+ it: () => listRunsBefore,
88
+ j: () => claimReadyRunForMachine,
89
+ jt: () => runningRunsForMachine,
90
+ k: () => blobSizes,
91
+ kt: () => requestRunCancel,
92
+ l: () => mintDeviceToken,
93
+ lt: () => openRuns,
94
+ m: () => retireLease,
95
+ mt: () => putReportReceiptIfAbsent,
96
+ n: () => isOutdated,
97
+ nt: () => listMachines,
98
+ o: () => connectKeyValid,
99
+ ot: () => loopIdsWithSnapshots,
100
+ p: () => resolveLease,
101
+ pt: () => pruneRunSnapshots,
102
+ q: () => getRun,
103
+ qt: () => logger,
104
+ r: () => isValidSemver,
105
+ rt: () => listRuns,
106
+ s: () => isDeviceTokenShape,
107
+ st: () => loopsForMachine,
108
+ t: () => validateLoopTags,
109
+ tt: () => listLoops,
110
+ u: () => pruneExpiredLeases,
111
+ ut: () => pauseLoop,
112
+ v: () => artifacts_exports,
113
+ vt: () => reclaimRun,
114
+ w: () => acknowledgeRetiredTerminalIncident,
115
+ wt: () => refreshRunHeartbeats,
116
+ x: () => safeRelPath,
117
+ xt: () => reconciliationStatesForRuns,
118
+ y: () => isValidHash,
119
+ yt: () => reconcileConfiguredArtifacts,
120
+ z: () => finalizeRunningRun,
121
+ zt: () => updateMachine
122
+ });
15
123
  var import_pino = /* @__PURE__ */ __toESM(require_pino());
16
124
  var import_pino_pretty = /* @__PURE__ */ __toESM(require_pino_pretty());
17
- function defineErrorCodes(codes) {
18
- return Object.fromEntries(Object.entries(codes).map(([key, value]) => [key, {
19
- code: key,
20
- message: value,
21
- toString: () => key
22
- }]));
23
- }
24
- var BASE_ERROR_CODES = defineErrorCodes({
25
- USER_NOT_FOUND: "User not found",
26
- FAILED_TO_CREATE_USER: "Failed to create user",
27
- FAILED_TO_CREATE_SESSION: "Failed to create session",
28
- FAILED_TO_UPDATE_USER: "Failed to update user",
29
- FAILED_TO_GET_SESSION: "Failed to get session",
30
- INVALID_PASSWORD: "Invalid password",
31
- INVALID_EMAIL: "Invalid email",
32
- INVALID_EMAIL_OR_PASSWORD: "Invalid email or password",
33
- INVALID_USER: "Invalid user",
34
- SOCIAL_ACCOUNT_ALREADY_LINKED: "Social account already linked",
35
- PROVIDER_NOT_FOUND: "Provider not found",
36
- INVALID_TOKEN: "Invalid token",
37
- TOKEN_EXPIRED: "Token expired",
38
- ID_TOKEN_NOT_SUPPORTED: "id_token not supported",
39
- FAILED_TO_GET_USER_INFO: "Failed to get user info",
40
- USER_EMAIL_NOT_FOUND: "User email not found",
41
- EMAIL_NOT_VERIFIED: "Email not verified",
42
- PASSWORD_TOO_SHORT: "Password too short",
43
- PASSWORD_TOO_LONG: "Password too long",
44
- USER_ALREADY_EXISTS: "User already exists.",
45
- USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "User already exists. Use another email.",
46
- EMAIL_CAN_NOT_BE_UPDATED: "Email can not be updated",
47
- CHANGE_EMAIL_DISABLED: "Change email is disabled",
48
- CREDENTIAL_ACCOUNT_NOT_FOUND: "Credential account not found",
49
- SESSION_EXPIRED: "Session expired. Re-authenticate to perform this action.",
50
- FAILED_TO_UNLINK_LAST_ACCOUNT: "You can't unlink your last account",
51
- ACCOUNT_NOT_FOUND: "Account not found",
52
- USER_ALREADY_HAS_PASSWORD: "User already has a password. Provide that to delete the account.",
53
- CROSS_SITE_NAVIGATION_LOGIN_BLOCKED: "Cross-site navigation login blocked. This request appears to be a CSRF attack.",
54
- VERIFICATION_EMAIL_NOT_ENABLED: "Verification email isn't enabled",
55
- EMAIL_ALREADY_VERIFIED: "Email is already verified",
56
- EMAIL_MISMATCH: "Email mismatch",
57
- SESSION_NOT_FRESH: "Session is not fresh",
58
- LINKED_ACCOUNT_ALREADY_EXISTS: "Linked account already exists",
59
- INVALID_ORIGIN: "Invalid origin",
60
- INVALID_CALLBACK_URL: "Invalid callbackURL",
61
- INVALID_REDIRECT_URL: "Invalid redirectURL",
62
- INVALID_ERROR_CALLBACK_URL: "Invalid errorCallbackURL",
63
- INVALID_NEW_USER_CALLBACK_URL: "Invalid newUserCallbackURL",
64
- MISSING_OR_NULL_ORIGIN: "Missing or null Origin",
65
- CALLBACK_URL_REQUIRED: "callbackURL is required",
66
- FAILED_TO_CREATE_VERIFICATION: "Unable to create verification",
67
- FIELD_NOT_ALLOWED: "Field not allowed to be set",
68
- ASYNC_VALIDATION_NOT_SUPPORTED: "Async validation is not supported",
69
- VALIDATION_ERROR: "Validation Error",
70
- MISSING_FIELD: "Field is required",
71
- METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED: "POST method requires deferSessionRefresh to be enabled in session config",
72
- BODY_MUST_BE_AN_OBJECT: "Body must be an object",
73
- PASSWORD_ALREADY_SET: "User already has a password set"
74
- });
75
- function isErrorStackTraceLimitWritable() {
76
- const desc = Object.getOwnPropertyDescriptor(Error, "stackTraceLimit");
77
- if (desc === void 0) return Object.isExtensible(Error);
78
- return Object.prototype.hasOwnProperty.call(desc, "writable") ? desc.writable : desc.set !== void 0;
79
- }
80
- /**
81
- * Hide internal stack frames from the error stack trace.
82
- */
83
- function hideInternalStackFrames(stack) {
84
- const lines = stack.split("\n at ");
85
- if (lines.length <= 1) return stack;
86
- lines.splice(1, 1);
87
- return lines.join("\n at ");
88
- }
89
- /**
90
- * Creates a custom error class that hides stack frames.
91
- */
92
- function makeErrorForHideStackFrame(Base, clazz) {
93
- class HideStackFramesError extends Base {
94
- #hiddenStack;
95
- constructor(...args) {
96
- if (isErrorStackTraceLimitWritable()) {
97
- const limit = Error.stackTraceLimit;
98
- Error.stackTraceLimit = 0;
99
- super(...args);
100
- Error.stackTraceLimit = limit;
101
- } else super(...args);
102
- const stack = (/* @__PURE__ */ new Error()).stack;
103
- if (stack) this.#hiddenStack = hideInternalStackFrames(stack.replace(/^Error/, this.name));
104
- }
105
- get errorStack() {
106
- return this.#hiddenStack;
107
- }
108
- }
109
- Object.defineProperty(HideStackFramesError.prototype, "constructor", {
110
- get() {
111
- return clazz;
112
- },
113
- enumerable: false,
114
- configurable: true
115
- });
116
- return HideStackFramesError;
117
- }
118
- var statusCodes = {
119
- OK: 200,
120
- CREATED: 201,
121
- ACCEPTED: 202,
122
- NO_CONTENT: 204,
123
- MULTIPLE_CHOICES: 300,
124
- MOVED_PERMANENTLY: 301,
125
- FOUND: 302,
126
- SEE_OTHER: 303,
127
- NOT_MODIFIED: 304,
128
- TEMPORARY_REDIRECT: 307,
129
- BAD_REQUEST: 400,
130
- UNAUTHORIZED: 401,
131
- PAYMENT_REQUIRED: 402,
132
- FORBIDDEN: 403,
133
- NOT_FOUND: 404,
134
- METHOD_NOT_ALLOWED: 405,
135
- NOT_ACCEPTABLE: 406,
136
- PROXY_AUTHENTICATION_REQUIRED: 407,
137
- REQUEST_TIMEOUT: 408,
138
- CONFLICT: 409,
139
- GONE: 410,
140
- LENGTH_REQUIRED: 411,
141
- PRECONDITION_FAILED: 412,
142
- PAYLOAD_TOO_LARGE: 413,
143
- URI_TOO_LONG: 414,
144
- UNSUPPORTED_MEDIA_TYPE: 415,
145
- RANGE_NOT_SATISFIABLE: 416,
146
- EXPECTATION_FAILED: 417,
147
- "I'M_A_TEAPOT": 418,
148
- MISDIRECTED_REQUEST: 421,
149
- UNPROCESSABLE_ENTITY: 422,
150
- LOCKED: 423,
151
- FAILED_DEPENDENCY: 424,
152
- TOO_EARLY: 425,
153
- UPGRADE_REQUIRED: 426,
154
- PRECONDITION_REQUIRED: 428,
155
- TOO_MANY_REQUESTS: 429,
156
- REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
157
- UNAVAILABLE_FOR_LEGAL_REASONS: 451,
158
- INTERNAL_SERVER_ERROR: 500,
159
- NOT_IMPLEMENTED: 501,
160
- BAD_GATEWAY: 502,
161
- SERVICE_UNAVAILABLE: 503,
162
- GATEWAY_TIMEOUT: 504,
163
- HTTP_VERSION_NOT_SUPPORTED: 505,
164
- VARIANT_ALSO_NEGOTIATES: 506,
165
- INSUFFICIENT_STORAGE: 507,
166
- LOOP_DETECTED: 508,
167
- NOT_EXTENDED: 510,
168
- NETWORK_AUTHENTICATION_REQUIRED: 511
169
- };
170
- var InternalAPIError = class extends Error {
171
- constructor(status = "INTERNAL_SERVER_ERROR", body = void 0, headers = {}, statusCode = typeof status === "number" ? status : statusCodes[status]) {
172
- super(body?.message, body?.cause ? { cause: body.cause } : void 0);
173
- this.status = status;
174
- this.body = body;
175
- this.headers = headers;
176
- this.statusCode = statusCode;
177
- this.name = "APIError";
178
- this.status = status;
179
- this.headers = headers;
180
- this.statusCode = statusCode;
181
- this.body = body;
182
- }
183
- };
184
- var ValidationError$1 = class extends InternalAPIError {
185
- constructor(message, issues) {
186
- super(400, {
187
- message,
188
- code: "VALIDATION_ERROR"
189
- });
190
- this.message = message;
191
- this.issues = issues;
192
- this.issues = issues;
193
- }
194
- };
195
- var BetterCallError = class extends Error {
196
- constructor(message) {
197
- super(message);
198
- this.name = "BetterCallError";
199
- }
200
- };
201
- var kAPIErrorHeaderSymbol = Symbol.for("better-call:api-error-headers");
202
- var APIError$1 = makeErrorForHideStackFrame(InternalAPIError, Error);
203
- var BetterAuthError = class extends Error {
204
- constructor(message, options) {
205
- super(message, options);
206
- this.name = "BetterAuthError";
207
- this.message = message;
208
- this.stack = "";
209
- }
210
- };
211
- var APIError = class APIError extends APIError$1 {
212
- constructor(...args) {
213
- super(...args);
214
- }
215
- static fromStatus(status, body) {
216
- return new APIError(status, body);
217
- }
218
- static from(status, error) {
219
- return new APIError(status, {
220
- message: error.message,
221
- code: error.code
222
- });
223
- }
224
- };
225
- var _envShim = Object.create(null);
226
- var _getEnv = (useShim) => globalThis.process?.env || globalThis.Deno?.env.toObject() || globalThis.__env__ || (useShim ? _envShim : globalThis);
227
- var env = new Proxy(_envShim, {
228
- get(_, prop) {
229
- return _getEnv()[prop] ?? _envShim[prop];
230
- },
231
- has(_, prop) {
232
- return prop in _getEnv() || prop in _envShim;
233
- },
234
- set(_, prop, value) {
235
- const env = _getEnv(true);
236
- env[prop] = value;
237
- return true;
238
- },
239
- deleteProperty(_, prop) {
240
- if (!prop) return false;
241
- const env = _getEnv(true);
242
- delete env[prop];
243
- return true;
244
- },
245
- ownKeys() {
246
- const env = _getEnv(true);
247
- return Object.keys(env);
248
- }
249
- });
250
- function toBoolean(val) {
251
- return val ? val !== "false" : false;
252
- }
253
- var nodeENV = env.NODE_ENV ?? "";
254
- /** Detect if `NODE_ENV` environment variable is `production` */
255
- var isProduction = nodeENV === "production";
256
- /** Detect if `NODE_ENV` environment variable is `dev` or `development` */
257
- var isDevelopment = () => nodeENV === "dev" || nodeENV === "development";
258
- /** Detect if `NODE_ENV` environment variable is `test` */
259
- var isTest = () => nodeENV === "test" || toBoolean(env.TEST);
260
- /**
261
- * Get environment variable with fallback
262
- */
263
- function getEnvVar(key, fallback) {
264
- if (typeof process !== "undefined" && process.env) return process.env[key] ?? fallback;
265
- if (typeof Deno !== "undefined") return Deno.env.get(key) ?? fallback;
266
- if (typeof Bun !== "undefined") return Bun.env[key] ?? fallback;
267
- return fallback;
268
- }
269
- /**
270
- * Get boolean environment variable
271
- */
272
- function getBooleanEnvVar(key, fallback = true) {
273
- const value = getEnvVar(key);
274
- if (!value) return fallback;
275
- return value !== "0" && value.toLowerCase() !== "false" && value !== "";
276
- }
277
- /**
278
- * Common environment variables used in Better Auth
279
- */
280
- var ENV = Object.freeze({
281
- get BETTER_AUTH_SECRET() {
282
- return getEnvVar("BETTER_AUTH_SECRET");
283
- },
284
- get AUTH_SECRET() {
285
- return getEnvVar("AUTH_SECRET");
286
- },
287
- get BETTER_AUTH_TELEMETRY() {
288
- return getEnvVar("BETTER_AUTH_TELEMETRY");
289
- },
290
- get BETTER_AUTH_TELEMETRY_ID() {
291
- return getEnvVar("BETTER_AUTH_TELEMETRY_ID");
292
- },
293
- get NODE_ENV() {
294
- return getEnvVar("NODE_ENV", "development");
295
- },
296
- get PACKAGE_VERSION() {
297
- return getEnvVar("PACKAGE_VERSION", "0.0.0");
298
- },
299
- get BETTER_AUTH_TELEMETRY_ENDPOINT() {
300
- return getEnvVar("BETTER_AUTH_TELEMETRY_ENDPOINT", "");
301
- }
302
- });
303
- function capitalizeFirstLetter(str) {
304
- return str.charAt(0).toUpperCase() + str.slice(1);
305
- }
306
- var WORD_PATTERN = /[\p{Ll}\d]+|\p{Lu}+(?!\p{Ll})|\p{Lu}[\p{Ll}\d]+|\p{Lo}+/gu;
307
- var APOSTROPHE_PATTERN = /['\u2019]/g;
308
- function splitWords(input) {
309
- return input.replace(APOSTROPHE_PATTERN, "").match(WORD_PATTERN) ?? [];
310
- }
311
- function toKebabCase(input) {
312
- return splitWords(input).map((word) => word.toLowerCase()).join("-");
313
- }
314
- /**
315
- * Escapes a character if it has a special meaning in regular expressions
316
- * and returns the character as is if it doesn't
317
- */
318
- function escapeRegExpChar(char) {
319
- if (char === "-" || char === "^" || char === "$" || char === "+" || char === "." || char === "(" || char === ")" || char === "|" || char === "[" || char === "]" || char === "{" || char === "}" || char === "*" || char === "?" || char === "\\") return `\\${char}`;
320
- else return char;
321
- }
322
- /**
323
- * Escapes all characters in a given string that have a special meaning in regular expressions
324
- */
325
- function escapeRegExpString(str) {
326
- let result = "";
327
- for (let i = 0; i < str.length; i++) result += escapeRegExpChar(str[i]);
328
- return result;
329
- }
330
- /**
331
- * Transforms one or more glob patterns into a RegExp pattern
332
- */
333
- function transform(pattern, separator = true) {
334
- if (Array.isArray(pattern)) return `(?:${pattern.map((p) => `^${transform(p, separator)}$`).join("|")})`;
335
- let separatorSplitter = "";
336
- let separatorMatcher = "";
337
- let wildcard = ".";
338
- if (separator === true) {
339
- separatorSplitter = "/";
340
- separatorMatcher = "[/\\\\]";
341
- wildcard = "[^/\\\\]";
342
- } else if (separator) {
343
- separatorSplitter = separator;
344
- separatorMatcher = escapeRegExpString(separatorSplitter);
345
- if (separatorMatcher.length > 1) {
346
- separatorMatcher = `(?:${separatorMatcher})`;
347
- wildcard = `((?!${separatorMatcher}).)`;
348
- } else wildcard = `[^${separatorMatcher}]`;
349
- }
350
- const requiredSeparator = separator ? `${separatorMatcher}+?` : "";
351
- const optionalSeparator = separator ? `${separatorMatcher}*?` : "";
352
- const segments = separator ? pattern.split(separatorSplitter) : [pattern];
353
- let result = "";
354
- for (let s = 0; s < segments.length; s++) {
355
- const segment = segments[s];
356
- const nextSegment = segments[s + 1];
357
- let currentSeparator = "";
358
- if (!segment && s > 0) continue;
359
- if (separator) if (s === segments.length - 1) currentSeparator = optionalSeparator;
360
- else if (nextSegment !== "**") currentSeparator = requiredSeparator;
361
- else currentSeparator = "";
362
- if (separator && segment === "**") {
363
- if (currentSeparator) {
364
- result += s === 0 ? "" : currentSeparator;
365
- result += `(?:${wildcard}*?${currentSeparator})*?`;
366
- }
367
- continue;
368
- }
369
- for (let c = 0; c < segment.length; c++) {
370
- const char = segment[c];
371
- if (char === "\\") {
372
- if (c < segment.length - 1) {
373
- result += escapeRegExpChar(segment[c + 1]);
374
- c++;
375
- }
376
- } else if (char === "?") result += wildcard;
377
- else if (char === "*") result += `${wildcard}*?`;
378
- else result += escapeRegExpChar(char);
379
- }
380
- result += currentSeparator;
381
- }
382
- return result;
383
- }
384
- function isMatch(regexp, sample) {
385
- if (typeof sample !== "string") throw new TypeError(`Sample must be a string, but ${typeof sample} given`);
386
- return regexp.test(sample);
387
- }
388
- /**
389
- * Compiles one or more glob patterns into a RegExp and returns an isMatch function.
390
- * The isMatch function takes a sample string as its only argument and returns `true`
391
- * if the string matches the pattern(s).
392
- *
393
- * ```js
394
- * wildcardMatch('src/*.js')('src/index.js') //=> true
395
- * ```
396
- *
397
- * ```js
398
- * const isMatch = wildcardMatch('*.example.com', '.')
399
- * isMatch('foo.example.com') //=> true
400
- * isMatch('foo.bar.com') //=> false
401
- * ```
402
- */
403
- function wildcardMatch(pattern, options) {
404
- 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`);
405
- if (typeof options === "string" || typeof options === "boolean") options = { separator: options };
406
- 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`);
407
- options = options || {};
408
- if (options.separator === "\\") throw new Error("\\ is not a valid separator because it is used for escaping. Try setting the separator to `true` instead");
409
- const regexpPattern = transform(pattern, options.separator);
410
- const regexp = new RegExp(`^${regexpPattern}$`, options.flags);
411
- const fn = isMatch.bind(null, regexp);
412
- fn.options = options;
413
- fn.pattern = pattern;
414
- fn.regexp = regexp;
415
- return fn;
416
- }
417
- var SLASH_CHAR_CODE = "/".charCodeAt(0);
418
- /**
419
- * Minimal loopback check for dev scheme inference only. Reachable from
420
- * `client/config.ts` via `getBaseURL`, so we MUST NOT import the full
421
- * `@better-auth/core/utils/host` classifier here: its `utils/ip` dependency
422
- * on zod would leak into the client bundle (see `e2e/smoke/test/vite.spec.ts`).
423
- *
424
- * Server-side SSRF/loopback checks (oauth redirect matching, trusted-origin
425
- * resolution, electron fetch gate) continue to use the authoritative
426
- * `isLoopbackHost` from `@better-auth/core/utils/host`. This helper's only
427
- * job is picking `http` vs `https` for dev ergonomics.
428
- */
429
- function isLoopbackForDevScheme(host) {
430
- const hostname = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, "").toLowerCase();
431
- return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || hostname.startsWith("127.");
432
- }
433
- function trimTrailingSlashes(value) {
434
- let end = value.length;
435
- while (end > 0 && value.charCodeAt(end - 1) === SLASH_CHAR_CODE) end--;
436
- return end === value.length ? value : value.slice(0, end);
437
- }
438
- function checkHasPath(url) {
439
- try {
440
- return (trimTrailingSlashes(new URL(url).pathname) || "/") !== "/";
441
- } catch {
442
- throw new BetterAuthError(`Invalid base URL: ${url}. Please provide a valid base URL.`);
443
- }
444
- }
445
- function assertHasProtocol(url) {
446
- try {
447
- const parsedUrl = new URL(url);
448
- if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") throw new BetterAuthError(`Invalid base URL: ${url}. URL must include 'http://' or 'https://'`);
449
- } catch (error) {
450
- if (error instanceof BetterAuthError) throw error;
451
- throw new BetterAuthError(`Invalid base URL: ${url}. Please provide a valid base URL.`, { cause: error });
452
- }
453
- }
454
- function withPath(url, path = "/api/auth") {
455
- assertHasProtocol(url);
456
- if (checkHasPath(url)) return url;
457
- const trimmedUrl = trimTrailingSlashes(url);
458
- if (!path || path === "/") return trimmedUrl;
459
- path = path.startsWith("/") ? path : `/${path}`;
460
- return `${trimmedUrl}${path}`;
461
- }
462
- function validateProxyHeader(header, type) {
463
- if (!header || header.trim() === "") return false;
464
- if (type === "proto") return header === "http" || header === "https";
465
- if (type === "host") {
466
- if ([
467
- /\.\./,
468
- /\0/,
469
- /[\s]/,
470
- /^[.]/,
471
- /[<>'"]/,
472
- /javascript:/i,
473
- /file:/i,
474
- /data:/i
475
- ].some((pattern) => pattern.test(header))) return false;
476
- 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);
477
- }
478
- return false;
479
- }
480
- function getBaseURL(url, path, request, loadEnv, trustedProxyHeaders) {
481
- if (url) return withPath(url, path);
482
- if (loadEnv !== false) {
483
- 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);
484
- if (fromEnv) return withPath(fromEnv, path);
485
- }
486
- const fromRequest = request?.headers.get("x-forwarded-host");
487
- const fromRequestProto = request?.headers.get("x-forwarded-proto");
488
- if (fromRequest && fromRequestProto && trustedProxyHeaders) {
489
- if (validateProxyHeader(fromRequestProto, "proto") && validateProxyHeader(fromRequest, "host")) try {
490
- return withPath(`${fromRequestProto}://${fromRequest}`, path);
491
- } catch (_error) {}
492
- }
493
- if (request) {
494
- const url = getOrigin(request.url);
495
- if (!url) throw new BetterAuthError("Could not get origin from request. Please provide a valid base URL.");
496
- return withPath(url, path);
497
- }
498
- if (typeof window !== "undefined" && window.location) return withPath(window.location.origin, path);
499
- }
500
- function getOrigin(url) {
501
- try {
502
- const parsedUrl = new URL(url);
503
- return parsedUrl.origin === "null" ? null : parsedUrl.origin;
504
- } catch {
505
- return null;
506
- }
507
- }
508
- function getProtocol(url) {
509
- try {
510
- return new URL(url).protocol;
511
- } catch {
512
- return null;
513
- }
514
- }
515
- function getHost(url) {
516
- try {
517
- return new URL(url).host;
518
- } catch {
519
- return null;
520
- }
521
- }
522
- /**
523
- * Checks if the baseURL config is a dynamic config object
524
- */
525
- function isDynamicBaseURLConfig(config) {
526
- return typeof config === "object" && config !== null && "allowedHosts" in config && Array.isArray(config.allowedHosts);
527
- }
528
- /**
529
- * Check if a value is a `Request`
530
- * - `instanceof`: works for native Request instances
531
- * - `toString`: handles where instanceof check fails but the object is still a
532
- * valid Request (e.g. cross-realm, polyfills). Paired with a shape check so
533
- * an object that only spoofs `Symbol.toStringTag` without the real shape is
534
- * rejected before downstream code tries to read `.headers` / `.url`.
535
- *
536
- * @param value The value to check
537
- * @returns `true` if the value is a Request instance
538
- */
539
- function isRequestLike(value) {
540
- if (value instanceof Request) return true;
541
- if (typeof value !== "object" || value === null || Object.prototype.toString.call(value) !== "[object Request]") return false;
542
- const v = value;
543
- return typeof v.url === "string" && typeof v.headers === "object" && v.headers !== null && typeof v.headers.get === "function";
544
- }
545
- /**
546
- * Extracts the host from a `Request` or `Headers`.
547
- * Honors `x-forwarded-host` only when `trustedProxyHeaders` is enabled,
548
- * then falls back to the `host` header and finally the request URL.
549
- */
550
- function getHostFromSource(source, trustedProxyHeaders) {
551
- const headers = isRequestLike(source) ? source.headers : source;
552
- if (trustedProxyHeaders) {
553
- const forwardedHost = headers.get("x-forwarded-host");
554
- if (forwardedHost && validateProxyHeader(forwardedHost, "host")) return forwardedHost;
555
- }
556
- const host = headers.get("host");
557
- if (host && validateProxyHeader(host, "host")) return host;
558
- if (isRequestLike(source)) try {
559
- return new URL(source.url).host;
560
- } catch {
561
- return null;
562
- }
563
- return null;
564
- }
565
- /**
566
- * Extracts the protocol from a `Request` or `Headers`.
567
- * Honors `x-forwarded-proto` only when `trustedProxyHeaders` is enabled,
568
- * then falls back to the request URL, then to "https".
569
- */
570
- function getProtocolFromSource(source, configProtocol, trustedProxyHeaders) {
571
- if (configProtocol === "http" || configProtocol === "https") return configProtocol;
572
- const headers = isRequestLike(source) ? source.headers : source;
573
- if (trustedProxyHeaders) {
574
- const forwardedProto = headers.get("x-forwarded-proto");
575
- if (forwardedProto && validateProxyHeader(forwardedProto, "proto")) return forwardedProto;
576
- }
577
- if (isRequestLike(source)) try {
578
- const url = new URL(source.url);
579
- if (url.protocol === "http:" || url.protocol === "https:") return url.protocol.slice(0, -1);
580
- } catch {}
581
- const host = getHostFromSource(source, trustedProxyHeaders);
582
- if (host && isLoopbackForDevScheme(host)) return "http";
583
- return "https";
584
- }
585
- /**
586
- * Matches a hostname against a host pattern.
587
- * Supports wildcard patterns like `*.vercel.app` or `preview-*.myapp.com`.
588
- *
589
- * @param host The hostname to test (e.g., "myapp.com", "preview-123.vercel.app")
590
- * @param pattern The host pattern (e.g., "myapp.com", "*.vercel.app")
591
- * @returns {boolean} true if the host matches the pattern, false otherwise.
592
- *
593
- * @example
594
- * ```ts
595
- * matchesHostPattern("myapp.com", "myapp.com") // true
596
- * matchesHostPattern("preview-123.vercel.app", "*.vercel.app") // true
597
- * matchesHostPattern("preview-123.myapp.com", "preview-*.myapp.com") // true
598
- * matchesHostPattern("evil.com", "myapp.com") // false
599
- * ```
600
- */
601
- var matchesHostPattern = (host, pattern) => {
602
- if (!host || !pattern) return false;
603
- const normalizedHost = host.replace(/^https?:\/\//, "").split("/")[0].toLowerCase();
604
- const normalizedPattern = pattern.replace(/^https?:\/\//, "").split("/")[0].toLowerCase();
605
- if (normalizedPattern.includes("*") || normalizedPattern.includes("?")) return wildcardMatch(normalizedPattern)(normalizedHost);
606
- return normalizedHost.toLowerCase() === normalizedPattern.toLowerCase();
607
- };
608
- /**
609
- * Resolves the base URL from a dynamic config based on the incoming request.
610
- * Validates the derived host against the allowedHosts allowlist.
611
- *
612
- * @param config The dynamic base URL config
613
- * @param request The incoming request
614
- * @param basePath The base path to append
615
- * @returns The resolved base URL with path
616
- * @throws BetterAuthError if host is not in allowedHosts and no fallback is set
617
- */
618
- function resolveDynamicBaseURL(config, source, basePath, trustedProxyHeaders) {
619
- const host = getHostFromSource(source, trustedProxyHeaders);
620
- if (!host) {
621
- if (config.fallback) return withPath(config.fallback, basePath);
622
- throw new BetterAuthError("Could not determine host from request headers. Please provide a fallback URL in your baseURL config.");
623
- }
624
- if (config.allowedHosts.some((pattern) => matchesHostPattern(host, pattern))) return withPath(`${getProtocolFromSource(source, config.protocol, trustedProxyHeaders)}://${host}`, basePath);
625
- if (config.fallback) return withPath(config.fallback, basePath);
626
- 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.`);
627
- }
628
- /**
629
- * Resolves the base URL from any config type (static string or dynamic object).
630
- * This is the main entry point for base URL resolution.
631
- *
632
- * @param config The base URL config (string or object)
633
- * @param basePath The base path to append
634
- * @param request Optional request for dynamic resolution
635
- * @param loadEnv Whether to load from environment variables
636
- * @param trustedProxyHeaders Whether to trust proxy headers (for legacy behavior)
637
- * @returns The resolved base URL with path
638
- */
639
- function resolveBaseURL(config, basePath, source, loadEnv, trustedProxyHeaders) {
640
- if (isDynamicBaseURLConfig(config)) {
641
- if (source) return resolveDynamicBaseURL(config, source, basePath, trustedProxyHeaders);
642
- if (config.fallback) return withPath(config.fallback, basePath);
643
- return getBaseURL(void 0, basePath, void 0, loadEnv, trustedProxyHeaders);
644
- }
645
- const request = isRequestLike(source) ? source : void 0;
646
- if (typeof config === "string") return getBaseURL(config, basePath, request, loadEnv, trustedProxyHeaders);
647
- return getBaseURL(void 0, basePath, request, loadEnv, trustedProxyHeaders);
648
- }
649
- /**
650
- * Normalizes a request pathname by removing the basePath prefix and trailing slashes.
651
- * This is useful for matching paths against configured path lists.
652
- *
653
- * @param requestUrl - The full request URL
654
- * @param basePath - The base path of the auth API (e.g., "/api/auth")
655
- * @returns The normalized path without basePath prefix or trailing slashes,
656
- * or "/" if URL parsing fails
657
- *
658
- * @example
659
- * normalizePathname("http://localhost:3000/api/auth/sso/saml2/callback/provider1", "/api/auth")
660
- * // Returns: "/sso/saml2/callback/provider1"
661
- *
662
- * normalizePathname("http://localhost:3000/sso/saml2/callback/provider1/", "/")
663
- * // Returns: "/sso/saml2/callback/provider1"
664
- */
665
- function normalizePathname(requestUrl, basePath) {
666
- let pathname;
667
- try {
668
- pathname = new URL(requestUrl).pathname.replace(/\/+$/, "") || "/";
669
- } catch {
670
- return "/";
671
- }
672
- const normalizedBasePath = basePath.replace(/\/+$/, "");
673
- if (normalizedBasePath === "") return pathname;
674
- if (pathname === normalizedBasePath) return "/";
675
- if (pathname.startsWith(normalizedBasePath + "/")) return pathname.slice(normalizedBasePath.length).replace(/\/+$/, "") || "/";
676
- return pathname;
677
- }
678
- /**
679
- * Schemes that execute or embed code when navigated to or accepted as a
680
- * redirect target. These are never safe as an OAuth `redirect_uri` or as a
681
- * client-side navigation target (`window.location.href`, `location.assign`, ...).
682
- */
683
- var DANGEROUS_URL_SCHEMES = [
684
- "javascript:",
685
- "data:",
686
- "vbscript:"
687
- ];
688
- /**
689
- * Returns `false` only when `value` is an absolute URL using a dangerous scheme
690
- * (`javascript:`, `data:`, `vbscript:`). Relative URLs (e.g. `/dashboard`) and
691
- * safe absolute schemes (`http`, `https`, custom app schemes such as
692
- * `myapp://`) return `true`.
693
- *
694
- * Use this to guard browser navigation sinks and any redirect target that may
695
- * originate from untrusted input. It is intentionally narrow: it blocks code
696
- * execution schemes without rejecting relative paths or mobile deep links.
697
- */
698
- function isSafeUrlScheme(value) {
699
- let parsed;
700
- try {
701
- parsed = new URL(value);
702
- } catch {
703
- return true;
704
- }
705
- return !DANGEROUS_URL_SCHEMES.includes(parsed.protocol);
706
- }
707
- function isPlainObject(value) {
708
- if (value === null || typeof value !== "object") return false;
709
- const prototype = Object.getPrototypeOf(value);
710
- if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) return false;
711
- if (Symbol.iterator in value) return false;
712
- if (Symbol.toStringTag in value) return Object.prototype.toString.call(value) === "[object Module]";
713
- return true;
714
- }
715
- function _defu(baseObject, defaults, namespace = ".", merger) {
716
- if (!isPlainObject(defaults)) return _defu(baseObject, {}, namespace, merger);
717
- const object = { ...defaults };
718
- for (const key of Object.keys(baseObject)) {
719
- if (key === "__proto__" || key === "constructor") continue;
720
- const value = baseObject[key];
721
- if (value === null || value === void 0) continue;
722
- if (merger && merger(object, key, value, namespace)) continue;
723
- if (Array.isArray(value) && Array.isArray(object[key])) object[key] = [...value, ...object[key]];
724
- else if (isPlainObject(value) && isPlainObject(object[key])) object[key] = _defu(value, object[key], (namespace ? `${namespace}.` : "") + key.toString(), merger);
725
- else object[key] = value;
726
- }
727
- return object;
728
- }
729
- function createDefu(merger) {
730
- return (...arguments_) => arguments_.reduce((p, c) => _defu(p, c, "", merger), {});
731
- }
732
- var defu = createDefu();
733
125
  var __defProp = Object.defineProperty;
734
- var __defProps = Object.defineProperties;
735
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
736
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
737
- var __hasOwnProp = Object.prototype.hasOwnProperty;
738
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
739
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
740
- enumerable: true,
741
- configurable: true,
742
- writable: true,
743
- value
744
- }) : obj[key] = value;
745
- var __spreadValues = (a, b) => {
746
- for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]);
747
- if (__getOwnPropSymbols) {
748
- for (var prop of __getOwnPropSymbols(b)) if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]);
749
- }
750
- return a;
751
- };
752
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
753
- var BetterFetchError = class extends Error {
754
- constructor(status, statusText, error) {
755
- super(statusText || status.toString(), { cause: error });
756
- this.status = status;
757
- this.statusText = statusText;
758
- this.error = error;
759
- Error.captureStackTrace(this, this.constructor);
760
- }
761
- };
762
- var initializePlugins = async (url, options) => {
763
- var _a, _b, _c, _d, _e, _f;
764
- let opts = options || {};
765
- const hooks = {
766
- onRequest: [options == null ? void 0 : options.onRequest],
767
- onResponse: [options == null ? void 0 : options.onResponse],
768
- onSuccess: [options == null ? void 0 : options.onSuccess],
769
- onError: [options == null ? void 0 : options.onError],
770
- onRetry: [options == null ? void 0 : options.onRetry]
771
- };
772
- if (!options || !(options == null ? void 0 : options.plugins)) return {
773
- url,
774
- options: opts,
775
- hooks
776
- };
777
- for (const plugin of (options == null ? void 0 : options.plugins) || []) {
778
- if (plugin.init) {
779
- const pluginRes = await ((_a = plugin.init) == null ? void 0 : _a.call(plugin, url.toString(), options));
780
- opts = pluginRes.options || opts;
781
- url = pluginRes.url;
782
- }
783
- hooks.onRequest.push((_b = plugin.hooks) == null ? void 0 : _b.onRequest);
784
- hooks.onResponse.push((_c = plugin.hooks) == null ? void 0 : _c.onResponse);
785
- hooks.onSuccess.push((_d = plugin.hooks) == null ? void 0 : _d.onSuccess);
786
- hooks.onError.push((_e = plugin.hooks) == null ? void 0 : _e.onError);
787
- hooks.onRetry.push((_f = plugin.hooks) == null ? void 0 : _f.onRetry);
788
- }
789
- return {
790
- url,
791
- options: opts,
792
- hooks
793
- };
794
- };
795
- var LinearRetryStrategy = class {
796
- constructor(options) {
797
- this.options = options;
798
- }
799
- shouldAttemptRetry(attempt, response) {
800
- if (this.options.shouldRetry) return Promise.resolve(attempt < this.options.attempts && this.options.shouldRetry(response));
801
- return Promise.resolve(attempt < this.options.attempts);
802
- }
803
- getDelay() {
804
- return this.options.delay;
805
- }
806
- };
807
- var ExponentialRetryStrategy = class {
808
- constructor(options) {
809
- this.options = options;
810
- }
811
- shouldAttemptRetry(attempt, response) {
812
- if (this.options.shouldRetry) return Promise.resolve(attempt < this.options.attempts && this.options.shouldRetry(response));
813
- return Promise.resolve(attempt < this.options.attempts);
814
- }
815
- getDelay(attempt) {
816
- return Math.min(this.options.maxDelay, this.options.baseDelay * 2 ** attempt);
817
- }
818
- };
819
- function createRetryStrategy(options) {
820
- if (typeof options === "number") return new LinearRetryStrategy({
821
- type: "linear",
822
- attempts: options,
823
- delay: 1e3
126
+ var __exportAll = (all, no_symbols) => {
127
+ let target = {};
128
+ for (var name in all) __defProp(target, name, {
129
+ get: all[name],
130
+ enumerable: true
824
131
  });
825
- switch (options.type) {
826
- case "linear": return new LinearRetryStrategy(options);
827
- case "exponential": return new ExponentialRetryStrategy(options);
828
- default: throw new Error("Invalid retry strategy");
829
- }
830
- }
831
- var getAuthHeader = async (options) => {
832
- const headers = {};
833
- const getValue = async (value) => typeof value === "function" ? await value() : value;
834
- if (options == null ? void 0 : options.auth) {
835
- if (options.auth.type === "Bearer") {
836
- const token = await getValue(options.auth.token);
837
- if (!token) return headers;
838
- headers["authorization"] = `Bearer ${token}`;
839
- } else if (options.auth.type === "Basic") {
840
- const [username, password] = await Promise.all([getValue(options.auth.username), getValue(options.auth.password)]);
841
- if (!username || !password) return headers;
842
- headers["authorization"] = `Basic ${btoa(`${username}:${password}`)}`;
843
- } else if (options.auth.type === "Custom") {
844
- const [prefix, value] = await Promise.all([getValue(options.auth.prefix), getValue(options.auth.value)]);
845
- if (!value) return headers;
846
- headers["authorization"] = `${prefix != null ? prefix : ""} ${value}`;
847
- }
848
- }
849
- return headers;
850
- };
851
- var JSON_RE = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(;.+)?$/i;
852
- function detectResponseType(request) {
853
- const _contentType = request.headers.get("content-type");
854
- const textTypes = /* @__PURE__ */ new Set([
855
- "image/svg",
856
- "application/xml",
857
- "application/xhtml",
858
- "application/html"
859
- ]);
860
- if (!_contentType) return "json";
861
- const contentType = _contentType.split(";").shift() || "";
862
- if (JSON_RE.test(contentType)) return "json";
863
- if (textTypes.has(contentType) || contentType.startsWith("text/")) return "text";
864
- return "blob";
865
- }
866
- function isJSONParsable(value) {
867
- try {
868
- JSON.parse(value);
869
- return true;
870
- } catch (error) {
871
- return false;
872
- }
873
- }
874
- function isJSONSerializable(value) {
875
- if (value === void 0) return false;
876
- const t = typeof value;
877
- if (t === "string" || t === "number" || t === "boolean" || t === null) return true;
878
- if (t !== "object") return false;
879
- if (Array.isArray(value)) return true;
880
- if (value.buffer) return false;
881
- return value.constructor && value.constructor.name === "Object" || typeof value.toJSON === "function";
882
- }
883
- function jsonParse(text) {
884
- try {
885
- return JSON.parse(text);
886
- } catch (error) {
887
- return text;
888
- }
889
- }
890
- function isFunction(value) {
891
- return typeof value === "function";
892
- }
893
- function getFetch(options) {
894
- if (options == null ? void 0 : options.customFetchImpl) return options.customFetchImpl;
895
- if (typeof globalThis !== "undefined" && isFunction(globalThis.fetch)) return globalThis.fetch;
896
- if (typeof window !== "undefined" && isFunction(window.fetch)) return window.fetch;
897
- throw new Error("No fetch implementation found");
898
- }
899
- function mergeHeaders(...sources) {
900
- const merged = {};
901
- for (const source of sources) {
902
- if (!source) continue;
903
- if (source instanceof Headers) source.forEach((value, key) => {
904
- merged[key] = value;
905
- });
906
- else {
907
- const entries = Array.isArray(source) ? source : Object.entries(source);
908
- for (const [key, value] of entries) if (value !== null && value !== void 0) merged[key] = value;
909
- }
910
- }
911
- return merged;
912
- }
913
- async function getHeaders(opts) {
914
- const headers = new Headers(mergeHeaders(opts == null ? void 0 : opts.headers, await getAuthHeader(opts)));
915
- if (!headers.has("content-type")) {
916
- const contentType = detectContentType(opts == null ? void 0 : opts.body);
917
- if (contentType) headers.set("content-type", contentType);
918
- }
919
- return headers;
920
- }
921
- function detectContentType(body) {
922
- if (isJSONSerializable(body)) return "application/json";
923
- return null;
924
- }
925
- function getMediaType(headers) {
926
- const contentType = headers.get("content-type");
927
- return contentType ? contentType.split(";")[0].trim().toLowerCase() : null;
928
- }
929
- function getBody(options, headers) {
930
- const { body } = options;
931
- if (!body) return null;
932
- if (!isJSONSerializable(body)) return body;
933
- if (typeof body === "string") return body;
934
- if (getMediaType(headers) === "application/x-www-form-urlencoded") return new URLSearchParams(body).toString();
935
- return JSON.stringify(body);
936
- }
937
- function getMethod(url, options) {
938
- var _a;
939
- if (options == null ? void 0 : options.method) return options.method.toUpperCase();
940
- if (url.startsWith("@")) {
941
- const pMethod = (_a = url.split("@")[1]) == null ? void 0 : _a.split("/")[0];
942
- if (!methods.includes(pMethod)) return (options == null ? void 0 : options.body) ? "POST" : "GET";
943
- return pMethod.toUpperCase();
944
- }
945
- return (options == null ? void 0 : options.body) ? "POST" : "GET";
946
- }
947
- function getTimeout(options, controller) {
948
- let abortTimeout;
949
- 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);
950
- return {
951
- abortTimeout,
952
- clearTimeout: () => {
953
- if (abortTimeout) clearTimeout(abortTimeout);
954
- }
955
- };
956
- }
957
- var ValidationError = class _ValidationError extends Error {
958
- constructor(issues, message) {
959
- super(message || JSON.stringify(issues, null, 2));
960
- this.issues = issues;
961
- Object.setPrototypeOf(this, _ValidationError.prototype);
962
- }
963
- };
964
- async function parseStandardSchema(schema, input) {
965
- const result = await schema["~standard"].validate(input);
966
- if (result.issues) throw new ValidationError(result.issues);
967
- return result.value;
968
- }
969
- var methods = [
970
- "get",
971
- "post",
972
- "put",
973
- "patch",
974
- "delete"
975
- ];
976
- var applySchemaPlugin = (config) => ({
977
- id: "apply-schema",
978
- name: "Apply Schema",
979
- version: "1.0.0",
980
- async init(url, options) {
981
- var _a, _b, _c, _d;
982
- const schema = ((_b = (_a = config.plugins) == null ? void 0 : _a.find((plugin) => {
983
- var _a2;
984
- return ((_a2 = plugin.schema) == null ? void 0 : _a2.config) ? url.startsWith(plugin.schema.config.baseURL || "") || url.startsWith(plugin.schema.config.prefix || "") : false;
985
- })) == null ? void 0 : _b.schema) || config.schema;
986
- if (schema) {
987
- let urlKey = url;
988
- if ((_c = schema.config) == null ? void 0 : _c.prefix) {
989
- if (urlKey.startsWith(schema.config.prefix)) {
990
- urlKey = urlKey.replace(schema.config.prefix, "");
991
- if (schema.config.baseURL) url = url.replace(schema.config.prefix, schema.config.baseURL);
992
- }
993
- }
994
- if ((_d = schema.config) == null ? void 0 : _d.baseURL) {
995
- if (urlKey.startsWith(schema.config.baseURL)) urlKey = urlKey.replace(schema.config.baseURL, "");
996
- }
997
- if (urlKey.startsWith("/") && urlKey.charAt(1) === "@") urlKey = urlKey.substring(1);
998
- const keySchema = schema.schema[urlKey];
999
- if (keySchema) {
1000
- let validatedHeaders = options == null ? void 0 : options.headers;
1001
- if (keySchema.headers && !(options == null ? void 0 : options.disableValidation)) {
1002
- const normalizedHeaders = {};
1003
- if (options == null ? void 0 : options.headers) {
1004
- if (options.headers instanceof Headers) options.headers.forEach((value, key) => {
1005
- normalizedHeaders[key.toLowerCase()] = value;
1006
- });
1007
- else if (typeof options.headers === "object") {
1008
- for (const [key, value] of Object.entries(options.headers)) if (value !== null && value !== void 0) normalizedHeaders[key.toLowerCase()] = value;
1009
- }
1010
- }
1011
- const validated = await parseStandardSchema(keySchema.headers, normalizedHeaders);
1012
- const finalHeaders = {};
1013
- for (const [key, value] of Object.entries(validated)) finalHeaders[key.toLowerCase()] = value;
1014
- validatedHeaders = finalHeaders;
1015
- }
1016
- let opts = __spreadProps(__spreadValues({}, options), {
1017
- method: keySchema.method,
1018
- output: keySchema.output,
1019
- headers: validatedHeaders
1020
- });
1021
- if (!(options == null ? void 0 : options.disableValidation)) opts = __spreadProps(__spreadValues({}, opts), {
1022
- body: keySchema.input ? await parseStandardSchema(keySchema.input, options == null ? void 0 : options.body) : options == null ? void 0 : options.body,
1023
- params: keySchema.params ? await parseStandardSchema(keySchema.params, options == null ? void 0 : options.params) : options == null ? void 0 : options.params,
1024
- query: keySchema.query ? await parseStandardSchema(keySchema.query, options == null ? void 0 : options.query) : options == null ? void 0 : options.query
1025
- });
1026
- return {
1027
- url,
1028
- options: opts
1029
- };
1030
- }
1031
- }
1032
- return {
1033
- url,
1034
- options
1035
- };
1036
- }
1037
- });
1038
- var createFetch = (config) => {
1039
- async function $fetch(url, options) {
1040
- const opts = __spreadProps(__spreadValues(__spreadValues({}, config), options), {
1041
- headers: mergeHeaders(config == null ? void 0 : config.headers, options == null ? void 0 : options.headers),
1042
- plugins: [
1043
- ...(config == null ? void 0 : config.plugins) || [],
1044
- applySchemaPlugin(config || {}),
1045
- ...(options == null ? void 0 : options.plugins) || []
1046
- ]
1047
- });
1048
- if (config == null ? void 0 : config.catchAllError) try {
1049
- return await betterFetch(url, opts);
1050
- } catch (error) {
1051
- return {
1052
- data: null,
1053
- error: {
1054
- status: 500,
1055
- statusText: "Fetch Error",
1056
- message: "Fetch related error. Captured by catchAllError option. See error property for more details.",
1057
- error
1058
- }
1059
- };
1060
- }
1061
- return await betterFetch(url, opts);
1062
- }
1063
- return $fetch;
1064
- };
1065
- var isReservedPathSegment = (value) => value === "." || value === "..";
1066
- function encodePathSegment(segment, pathParams) {
1067
- let pathSegment = segment;
1068
- for (const [key, value] of pathParams) pathSegment = pathSegment.replace(key, value);
1069
- if (isReservedPathSegment(pathSegment)) throw new TypeError("Path parameters cannot be reserved path segments");
1070
- return encodeURIComponent(pathSegment);
1071
- }
1072
- function getURL2(url, option) {
1073
- const { baseURL, params, query } = option || {
1074
- query: {},
1075
- params: {},
1076
- baseURL: ""
1077
- };
1078
- let basePath = url.startsWith("http") ? url.split("/").slice(0, 3).join("/") : baseURL || "";
1079
- if (url.startsWith("@")) {
1080
- const m = url.toString().split("@")[1].split("/")[0];
1081
- if (methods.includes(m)) url = url.replace(`@${m}/`, "/");
1082
- }
1083
- if (!basePath.endsWith("/")) basePath += "/";
1084
- let [path, urlQuery] = url.replace(basePath, "").split("?");
1085
- const queryParams = new URLSearchParams(urlQuery);
1086
- for (const [key, value] of Object.entries(query || {})) {
1087
- if (value == null) continue;
1088
- let serializedValue;
1089
- if (typeof value === "string") serializedValue = value;
1090
- else if (Array.isArray(value)) {
1091
- for (const val of value) queryParams.append(key, val);
1092
- continue;
1093
- } else serializedValue = JSON.stringify(value);
1094
- queryParams.set(key, serializedValue);
1095
- }
1096
- const pathParams = /* @__PURE__ */ new Map();
1097
- if (params) if (Array.isArray(params)) {
1098
- const paramPaths = path.split("/").filter((p) => p.startsWith(":"));
1099
- for (const [index, key] of paramPaths.entries()) {
1100
- const value = params[index];
1101
- pathParams.set(key, String(value));
1102
- }
1103
- } else for (const [key, value] of Object.entries(params)) pathParams.set(`:${key}`, String(value));
1104
- path = path.split("/").map((segment) => encodePathSegment(segment, pathParams)).join("/");
1105
- path = path.replace(/^\/+/, "");
1106
- let queryParamString = queryParams.toString();
1107
- queryParamString = queryParamString.length > 0 ? `?${queryParamString}`.replace(/\+/g, "%20") : "";
1108
- if (!basePath.startsWith("http")) return `${basePath}${path}${queryParamString}`;
1109
- return new URL(`${path}${queryParamString}`, basePath);
1110
- }
1111
- var betterFetch = async (url, options) => {
1112
- var _a, _b, _c, _d, _e, _f, _g, _h;
1113
- const { hooks, url: __url, options: opts } = await initializePlugins(url, options);
1114
- const fetch = getFetch(opts);
1115
- const controller = new AbortController();
1116
- const signal = (_a = opts.signal) != null ? _a : controller.signal;
1117
- const _url = getURL2(__url, opts);
1118
- const headers = await getHeaders(opts);
1119
- const body = getBody(opts, headers);
1120
- const method = getMethod(__url, opts);
1121
- const context = __spreadProps(__spreadValues({}, opts), {
1122
- url: _url,
1123
- headers,
1124
- body,
1125
- method,
1126
- signal
1127
- });
1128
- for (const onRequest of hooks.onRequest) if (onRequest) {
1129
- const res = await onRequest(context);
1130
- if (typeof res === "object" && res !== null) Object.assign(context, res);
1131
- }
1132
- if ("pipeTo" in context && typeof context.pipeTo === "function" || typeof ((_b = options == null ? void 0 : options.body) == null ? void 0 : _b.pipe) === "function") {
1133
- if (!("duplex" in context)) context.duplex = "half";
1134
- }
1135
- const { clearTimeout: clearTimeout2 } = getTimeout(opts, controller);
1136
- let response = await fetch(context.url, context);
1137
- clearTimeout2();
1138
- const responseContext = {
1139
- response,
1140
- request: context
1141
- };
1142
- for (const onResponse of hooks.onResponse) if (onResponse) {
1143
- const r = await onResponse(__spreadProps(__spreadValues({}, responseContext), { response: ((_c = options == null ? void 0 : options.hookOptions) == null ? void 0 : _c.cloneResponse) ? response.clone() : response }));
1144
- if (r instanceof Response) response = r;
1145
- else if (typeof r === "object" && r !== null) response = r.response;
1146
- }
1147
- if (response.ok) {
1148
- if (!(context.method !== "HEAD")) return {
1149
- data: "",
1150
- error: null
1151
- };
1152
- const responseType = detectResponseType(response);
1153
- const successContext = {
1154
- data: null,
1155
- response,
1156
- request: context
1157
- };
1158
- if (responseType === "json" || responseType === "text") {
1159
- const text = await response.text();
1160
- successContext.data = await ((_d = context.jsonParser) != null ? _d : jsonParse)(text);
1161
- } else successContext.data = await response[responseType]();
1162
- if (context == null ? void 0 : context.output) {
1163
- if (context.output && !context.disableValidation) successContext.data = await parseStandardSchema(context.output, successContext.data);
1164
- }
1165
- 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 }));
1166
- if (options == null ? void 0 : options.throw) return successContext.data;
1167
- return {
1168
- data: successContext.data,
1169
- error: null
1170
- };
1171
- }
1172
- const parser = (_f = options == null ? void 0 : options.jsonParser) != null ? _f : jsonParse;
1173
- const responseText = await response.text();
1174
- const isJSONResponse = isJSONParsable(responseText);
1175
- const errorObject = isJSONResponse ? await parser(responseText) : null;
1176
- const errorContext = {
1177
- response,
1178
- responseText,
1179
- request: context,
1180
- error: __spreadProps(__spreadValues({}, errorObject), {
1181
- status: response.status,
1182
- statusText: response.statusText
1183
- })
1184
- };
1185
- 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 }));
1186
- if (options == null ? void 0 : options.retry) {
1187
- const retryStrategy = createRetryStrategy(options.retry);
1188
- const _retryAttempt = (_h = options.retryAttempt) != null ? _h : 0;
1189
- if (await retryStrategy.shouldAttemptRetry(_retryAttempt, response)) {
1190
- for (const onRetry of hooks.onRetry) if (onRetry) await onRetry(responseContext);
1191
- const delay = retryStrategy.getDelay(_retryAttempt);
1192
- await new Promise((resolve) => setTimeout(resolve, delay));
1193
- return await betterFetch(url, __spreadProps(__spreadValues({}, options), { retryAttempt: _retryAttempt + 1 }));
1194
- }
1195
- }
1196
- if (options == null ? void 0 : options.throw) throw new BetterFetchError(response.status, response.statusText, isJSONResponse ? errorObject : responseText);
1197
- return {
1198
- data: null,
1199
- error: __spreadProps(__spreadValues({}, errorObject), {
1200
- status: response.status,
1201
- statusText: response.statusText
1202
- })
1203
- };
132
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
133
+ return target;
1204
134
  };
1205
135
  /**
1206
- * Pievo server data directory — server-side file state, and (for the embedded
1207
- * pglite tier) the on-disk Postgres data dir at `<dataDir>/pgdata`. Locally it
1208
- * defaults to `~/.pievo`. Override with `PIEVO_DATA_DIR`. A hosted-Postgres tier
1209
- * still uses `<dataDir>/blobs` unless R2 is configured.
136
+ * Server data directory: the embedded database (`pgdata/`), artifact bytes
137
+ * (`blobs/`), and the launcher's pid/lock/log files. Kept apart from the
138
+ * daemon's `~/.pievo` so a device running both never mixes their state.
139
+ * Override with `PIEVO_DATA_DIR` (absolute; `~` is not expanded).
1210
140
  */
1211
141
  function dataDir() {
1212
- return process.env.PIEVO_DATA_DIR?.trim() || path.join(os.homedir(), ".pievo");
1213
- }
1214
- /**
1215
- * Supabase (or any Postgres) connection string for the RUNTIME app process. When
1216
- * set, the server uses the postgres-js driver (hosted prod/staging). Pool options
1217
- * are MODE-AWARE (`db/poolOptions.ts`): a Supabase TRANSACTION pooler URL
1218
- * (`:6543`) gets `prepare:false`; a SESSION pooler / direct URL (`:5432`) gets
1219
- * `prepare:true` (backend pinned per connection — safe and faster). Either form
1220
- * is supported; `dbPoolMode()` below overrides the port-based detection.
1221
- * When UNSET, the server falls back to the embedded, file-backed pglite database
1222
- * at `<dataDir>/pgdata` (local dev + light self-host + tests) — zero external DB.
1223
- */
1224
- function databaseUrl() {
1225
- return process.env.DATABASE_URL?.trim() || void 0;
1226
- }
1227
- /**
1228
- * Explicit pool-mode override for the runtime postgres-js pool
1229
- * (`PIEVO_DB_POOL_MODE=transaction|session`). Normally UNSET — the mode is
1230
- * detected from `DATABASE_URL`'s port (`:6543` ⇒ transaction pooler; see
1231
- * `db/poolOptions.ts` `isTransactionPooler`). This is the escape hatch for a
1232
- * NONSTANDARD-PORT or self-hosted pooler the port heuristic can't classify.
1233
- * Getting it wrong on a real transaction pooler (`session` when the pooler is
1234
- * transaction-mode) re-arms the prepared-statement breakage the detection
1235
- * exists to prevent — set it only when you know the pooler's mode.
1236
- */
1237
- function dbPoolMode() {
1238
- const raw = process.env.PIEVO_DB_POOL_MODE?.trim().toLowerCase();
1239
- if (raw === "transaction" || raw === "session") return raw;
1240
- }
1241
- function r2Config() {
1242
- const accountId = process.env.PIEVO_R2_ACCOUNT_ID?.trim();
1243
- const bucket = process.env.PIEVO_R2_BUCKET?.trim();
1244
- const accessKeyId = process.env.PIEVO_R2_ACCESS_KEY_ID?.trim();
1245
- const secretAccessKey = process.env.PIEVO_R2_SECRET_ACCESS_KEY?.trim();
1246
- const explicitEndpoint = process.env.PIEVO_R2_ENDPOINT?.trim();
1247
- const region = process.env.PIEVO_R2_REGION?.trim();
1248
- const endpoint = explicitEndpoint || (accountId ? `https://${accountId}.r2.cloudflarestorage.com` : void 0);
1249
- if (!!!(accountId || bucket || accessKeyId || secretAccessKey || explicitEndpoint || region)) return null;
1250
- const missing = [
1251
- !bucket && "PIEVO_R2_BUCKET",
1252
- !accessKeyId && "PIEVO_R2_ACCESS_KEY_ID",
1253
- !secretAccessKey && "PIEVO_R2_SECRET_ACCESS_KEY",
1254
- !endpoint && "PIEVO_R2_ACCOUNT_ID or PIEVO_R2_ENDPOINT"
1255
- ].filter(Boolean);
1256
- if (missing.length) throw new Error(`incomplete R2 configuration — missing ${missing.join(", ")}`);
1257
- return {
1258
- bucket,
1259
- endpoint,
1260
- accessKeyId,
1261
- secretAccessKey,
1262
- region: region || "auto"
1263
- };
1264
- }
1265
- /**
1266
- * Artifact-storage retention / GC knobs (see gateway/retention.ts). Read lazily
1267
- * from env at call time (not at module load) so tests can set them per-case and
1268
- * a deploy can tune them without a rebuild. Each has a safe, generous default —
1269
- * the bias is "keep storage bounded without ever surprising a healthy loop".
1270
- */
1271
- function posIntEnv(name, fallback) {
1272
- const raw = process.env[name]?.trim();
1273
- if (!raw) return fallback;
1274
- const n = Number(raw);
1275
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
1276
- }
1277
- /**
1278
- * How many of the most recent run snapshots to keep per loop. Older ones are
1279
- * pruned (their now-unreferenced blobs then become GC-collectable). The per-run
1280
- * diff only needs the immediately-prior snapshot, so 20 keeps ample history for
1281
- * the "Changes" view while bounding the table + blob retention. Default 20.
1282
- */
1283
- function snapshotRetention() {
1284
- return posIntEnv("PIEVO_SNAPSHOT_RETENTION", 20);
1285
- }
1286
- /**
1287
- * GC grace window: a blob whose metadata row is younger than this is NEVER
1288
- * collected, even if currently unreferenced. This is the concurrency guard —
1289
- * a blob a sync just wrote (and is about to reference) can't be swept out from
1290
- * under it. Generous default (1h) since a leaked blob is only a cost bug that
1291
- * the next GC pass reclaims anyway. Default 3,600,000ms (1 hour).
1292
- */
1293
- function blobGcGraceMs() {
1294
- return posIntEnv("PIEVO_BLOB_GC_GRACE_MS", 3600 * 1e3);
1295
- }
1296
- /**
1297
- * How often the periodic storage-maintenance pass runs (prune snapshots → GC
1298
- * blobs). Default 15 minutes. Kept independent of the faster offline-sweep tick.
1299
- */
1300
- function gcIntervalMs() {
1301
- return posIntEnv("PIEVO_GC_INTERVAL_MS", 900 * 1e3);
1302
- }
1303
- /**
1304
- * DB watchdog (`server/dbWatchdog.ts`) — the ACTUAL auto-recovery backstop for a
1305
- * wedged postgres-js pool. Fly's `[http_service.checks]` failing only pulls the
1306
- * machine from load balancing; it never restarts the VM (the 2026-07-12 outage:
1307
- * the pool wedged, `/api/health/db` hung, the check went critical, and the box
1308
- * stayed down ~9h with no auto-restart). The watchdog turns a HANG into a process
1309
- * EXIT so Fly's `restart.policy = "on-failure"` kicks in with a fresh pool.
1310
- *
1311
- * Enabled by default ONLY on the hosted postgres tier (a wedged transaction-pooler
1312
- * connection is the failure mode); OFF for the embedded pglite tier and under
1313
- * vitest (a test must never `process.exit`). Force either way with
1314
- * `PIEVO_DB_WATCHDOG=on|off`.
1315
- */
1316
- function dbWatchdogEnabled() {
1317
- const raw = process.env.PIEVO_DB_WATCHDOG?.trim().toLowerCase();
1318
- if (raw === "on") return true;
1319
- if (raw === "off") return false;
1320
- if (process.env.VITEST || false) return false;
1321
- return Boolean(databaseUrl());
1322
- }
1323
- function dbWatchdogIntervalMs() {
1324
- return posIntEnv("PIEVO_DB_WATCHDOG_INTERVAL_MS", 2e4);
1325
- }
1326
- function dbWatchdogTimeoutMs() {
1327
- return posIntEnv("PIEVO_DB_WATCHDOG_TIMEOUT_MS", 5e3);
1328
- }
1329
- /**
1330
- * Consecutive failed/timed-out pings before the watchdog exits the process. A
1331
- * healthy ping resets the counter, so this only trips on a SUSTAINED wedge — a
1332
- * brief blip never restarts the box. Default 3 (≈60-75s to detect at 20s cadence).
1333
- */
1334
- function dbWatchdogFailureThreshold() {
1335
- return posIntEnv("PIEVO_DB_WATCHDOG_FAILURES", 3);
142
+ return process.env.PIEVO_DATA_DIR?.trim() || path.join(os.homedir(), ".pievo-server");
1336
143
  }
1337
144
  /**
1338
- * Uses pino-pretty as a *synchronous* destination stream (not a worker-thread
1339
- * transport) so logs flush before process.exit() in the one-shot CLIs. Writes to
1340
- * stderr (fd 2) to keep stdout clean for QR codes and human CLI output.
1341
- */
1342
- var level = (process.env.PIEVO_LOG_LEVEL || "info").toLowerCase();
1343
- var stream = (0, import_pino_pretty.default)({
1344
- destination: 2,
1345
- colorize: true,
1346
- translateTime: "SYS:HH:MM:ss",
1347
- ignore: "pid,hostname"
1348
- });
1349
- var logger = (0, import_pino.default)({ level }, stream);
1350
- /**
1351
- * Pure pool-option derivation for the hosted postgres-js tier. LEAF module by
1352
- * design: no env reads, no DB opens — `db/index.ts` supplies the URL (and the
1353
- * optional `PIEVO_DB_POOL_MODE` override via `env.ts`), tests import this
1354
- * directly without booting a database.
1355
- */
1356
- /**
1357
- * Whether `url` points at a pgBouncer/Supavisor TRANSACTION pooler. Detection
1358
- * is by the conventional port `:6543` (Supabase's transaction-mode port).
1359
- *
1360
- * Why it matters: postgres-js pipelines queries and caches prepared statements
1361
- * per connection, but a transaction pooler reassigns the backend connection per
1362
- * transaction, which both breaks cached prepared statements and is implicated
1363
- * in the pool-wedge failure mode (porsager/postgres#970). Session mode (:5432)
1364
- * pins one backend per client connection, so neither problem exists there.
1365
- *
1366
- * LIMITATION: a nonstandard-port or self-hosted transaction pooler will NOT be
1367
- * detected here — set `PIEVO_DB_POOL_MODE=transaction` (see `env.ts`
1368
- * `dbPoolMode`) to override. Getting that wrong on a real transaction pooler
1369
- * re-arms the prepared-statement breakage.
1370
- */
1371
- function isTransactionPooler(url) {
1372
- try {
1373
- return new URL(url).port === "6543";
1374
- } catch {
1375
- return false;
1376
- }
1377
- }
145
+ * A blob whose metadata row is younger than this is never collected, so a
146
+ * sync that just wrote a blob can still reference it. A leaked blob is only a
147
+ * storage cost that the next pass reclaims. 1 hour.
148
+ */
149
+ var BLOB_GC_GRACE_MS = 3600 * 1e3;
150
+ /** Storage-maintenance cadence (prune snapshots, then GC blobs). 15 minutes. */
151
+ var GC_INTERVAL_MS = 900 * 1e3;
1378
152
  /**
1379
- * Mode-aware pool options:
1380
- *
1381
- * - TRANSACTION pooler (:6543): `prepare:false` is mandatory — the backend is
1382
- * reassigned per transaction, so cached prepared statements break.
1383
- * - SESSION pooler / direct (:5432): the backend is pinned per connection, so
1384
- * `prepare:true` is safe AND faster (cached plans), and the session-scoped
1385
- * `statement_timeout` is actually enforced on the executing backend.
1386
- *
1387
- * The shared knobs are the conservative self-healing ring for one always-on
1388
- * machine (see `db/index.ts` + api.health.db.ts for the incident history).
1389
- * Pass `transactionPooler` explicitly to override the port-based detection
1390
- * (the `PIEVO_DB_POOL_MODE` escape hatch).
153
+ * Uses pino-pretty as a *synchronous* destination stream (not a worker-thread
154
+ * transport) so logs flush before process.exit() in the one-shot CLIs. Writes to
155
+ * stderr (fd 2) to keep stdout clean for QR codes and human CLI output.
1391
156
  */
1392
- function poolOptionsFor(url, transactionPooler = isTransactionPooler(url)) {
1393
- return {
1394
- prepare: !transactionPooler,
1395
- max: 10,
1396
- idle_timeout: 30,
1397
- connect_timeout: 15,
1398
- max_lifetime: 1800,
1399
- connection: { statement_timeout: 3e4 }
1400
- };
1401
- }
157
+ var logger_exports = /* @__PURE__ */ __exportAll({ logger: () => logger });
158
+ var level = (process.env.PIEVO_LOG_LEVEL || "info").toLowerCase();
159
+ var stream = (0, import_pino_pretty.default)({
160
+ destination: 2,
161
+ colorize: true,
162
+ translateTime: "SYS:HH:MM:ss",
163
+ ignore: "pid,hostname"
164
+ });
165
+ var logger = (0, import_pino.default)({ level }, stream);
1402
166
  /** The coding agent a loop is bound to AND executed with (BYOA on the owner's
1403
167
  * machine): `claude-code` → Claude Code, `codex` → `codex exec`, `pi` → Pi.
1404
168
  * Non-Claude agents may still have thinner daemon telemetry until stream
@@ -1418,21 +182,23 @@ var CODING_AGENTS = [
1418
182
  function coerceCodingAgent(value) {
1419
183
  return typeof value === "string" && CODING_AGENTS.includes(value) ? value : null;
1420
184
  }
185
+ var RESERVED_LOOP_TAGS = [
186
+ "all loops",
187
+ "active",
188
+ "paused",
189
+ "blocked"
190
+ ];
1421
191
  /**
1422
- * Pievo business schema (Drizzle, Postgres `pg-core` dialect).
1423
- *
1424
- * Core machine / loop / run tables live alongside Better Auth ownership data.
1425
- * `userId` is the owning user's id. We use
1426
- * Drizzle (not raw SQL) so the store is single-sourced across the driver tiers
1427
- * (postgres-js on Supabase; embedded pglite for local/self-host + tests) — the
1428
- * query-builder API is identical, only `db/index.ts` branches the driver.
1429
- *
1430
- * Timestamp values are represented as ISO strings across both drivers. JSON
1431
- * columns use `jsonb().$type<>()`; booleans use native `boolean()`.
192
+ * Pievo schema (Drizzle, Postgres `pg-core` dialect, served by embedded PGlite).
193
+ * Pievo is single-user: no row carries an owner. Timestamp values are ISO
194
+ * strings; JSON columns use `jsonb().$type<>()`.
1432
195
  */
196
+ /** `skipped`: a pending run retired without executing (pause/retention cleanup,
197
+ * queue coalescing, or the offline catch-up window elapsed).
198
+ * Neither success nor failure — excluded from the failure streak (it rides
199
+ * phase `canceled`, and the streak counts only phase `error`). */
1433
200
  var machines = pgTable("machines", {
1434
201
  id: text("id").primaryKey(),
1435
- userId: text("user_id").notNull(),
1436
202
  name: text("name").notNull(),
1437
203
  hostname: text("hostname"),
1438
204
  platform: text("platform"),
@@ -1451,13 +217,13 @@ var machines = pgTable("machines", {
1451
217
  lastSeen: text("last_seen"),
1452
218
  online: boolean("online").notNull().default(false),
1453
219
  createdAt: text("created_at").notNull()
1454
- }, (t) => [index("machines_user_idx").on(t.userId)]);
220
+ });
1455
221
  var loops = pgTable("loops", {
1456
222
  id: text("id").primaryKey(),
1457
- userId: text("user_id").notNull(),
1458
223
  /** Execution machine (set at creation; no cross-machine fallback). */
1459
224
  machineId: text("machine_id").notNull(),
1460
225
  name: text("name").notNull(),
226
+ tags: text("tags").array().notNull().default(sql`ARRAY[]::text[]`),
1461
227
  prompt: text("prompt").notNull(),
1462
228
  statusKeep: text("status_keep").notNull(),
1463
229
  statusNoChange: text("status_no_change").notNull(),
@@ -1495,10 +261,11 @@ var loops = pgTable("loops", {
1495
261
  createdAt: text("created_at").notNull(),
1496
262
  updatedAt: text("updated_at").notNull()
1497
263
  }, (t) => [
1498
- index("loops_user_idx").on(t.userId),
1499
264
  index("loops_machine_idx").on(t.machineId),
1500
265
  index("delete_requested_loops").on(t.deleteRequestedAt).where(sql`${t.deleteRequestedAt} IS NOT NULL`),
1501
266
  check("loops_agent_check", sql`${t.agent} IN ('claude-code', 'codex', 'pi')`),
267
+ check("loops_tags_count_check", sql`cardinality(${t.tags}) <= 4`),
268
+ check("loops_tags_reserved_check", sql`NOT (${t.tags} && ARRAY['all loops', 'active', 'paused', 'blocked']::text[])`),
1502
269
  check("loops_schedule_mode_check", sql`${t.scheduleMode} IN ('cron', 'continuous')`),
1503
270
  check("loops_cron_overlap_check", sql`${t.cronOverlap} IN ('skip', 'queue-one')`)
1504
271
  ]);
@@ -1607,7 +374,6 @@ var terminalReportIncidents = pgTable("terminal_report_incidents", {
1607
374
  ]);
1608
375
  var connectKeys = pgTable("connect_keys", {
1609
376
  machineId: text("machine_id").primaryKey(),
1610
- userId: text("user_id").notNull(),
1611
377
  /** Mint time (ISO) — drives the TTL. */
1612
378
  mintedAt: text("minted_at").notNull()
1613
379
  });
@@ -1656,80 +422,11 @@ var runSnapshots = pgTable("run_snapshots", {
1656
422
  manifest: jsonb("manifest").$type().notNull(),
1657
423
  createdAt: text("created_at").notNull()
1658
424
  }, (t) => [index("run_snapshots_loop_idx").on(t.loopId)]);
1659
- var user = pgTable("user", {
1660
- id: text("id").primaryKey(),
1661
- name: text("name").notNull(),
1662
- email: text("email").notNull().unique(),
1663
- emailVerified: boolean("email_verified").default(false).notNull(),
1664
- image: text("image"),
1665
- createdAt: timestamp("created_at").defaultNow().notNull(),
1666
- updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()).notNull()
1667
- });
1668
- var session = pgTable("session", {
1669
- id: text("id").primaryKey(),
1670
- expiresAt: timestamp("expires_at").notNull(),
1671
- token: text("token").notNull().unique(),
1672
- createdAt: timestamp("created_at").defaultNow().notNull(),
1673
- updatedAt: timestamp("updated_at").$onUpdate(() => /* @__PURE__ */ new Date()).notNull(),
1674
- ipAddress: text("ip_address"),
1675
- userAgent: text("user_agent"),
1676
- userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" })
1677
- }, (table) => [index("session_userId_idx").on(table.userId)]);
1678
- var account = pgTable("account", {
1679
- id: text("id").primaryKey(),
1680
- accountId: text("account_id").notNull(),
1681
- providerId: text("provider_id").notNull(),
1682
- userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
1683
- accessToken: text("access_token"),
1684
- refreshToken: text("refresh_token"),
1685
- idToken: text("id_token"),
1686
- accessTokenExpiresAt: timestamp("access_token_expires_at"),
1687
- refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
1688
- scope: text("scope"),
1689
- password: text("password"),
1690
- createdAt: timestamp("created_at").defaultNow().notNull(),
1691
- updatedAt: timestamp("updated_at").$onUpdate(() => /* @__PURE__ */ new Date()).notNull()
1692
- }, (table) => [index("account_userId_idx").on(table.userId)]);
1693
- var verification = pgTable("verification", {
1694
- id: text("id").primaryKey(),
1695
- identifier: text("identifier").notNull(),
1696
- value: text("value").notNull(),
1697
- expiresAt: timestamp("expires_at").notNull(),
1698
- createdAt: timestamp("created_at").defaultNow().notNull(),
1699
- updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()).notNull()
1700
- }, (table) => [index("verification_identifier_idx").on(table.identifier)]);
1701
- relations(user, ({ many }) => ({
1702
- sessions: many(session),
1703
- accounts: many(account)
1704
- }));
1705
- relations(session, ({ one }) => ({ user: one(user, {
1706
- fields: [session.userId],
1707
- references: [user.id]
1708
- }) }));
1709
- relations(account, ({ one }) => ({ user: one(user, {
1710
- fields: [account.userId],
1711
- references: [user.id]
1712
- }) }));
1713
425
  /**
1714
- * Postgres connection + Drizzle handle. The driver is chosen at boot from env
1715
- * from environment configuration:
1716
- *
1717
- * - `DATABASE_URL` SET → postgres-js against Supabase (hosted prod/staging).
1718
- * Pool options are mode-aware (`db/poolOptions.ts`):
1719
- * `prepare:false` on the `:6543` transaction pooler,
1720
- * `prepare:true` on a `:5432` session/direct URL.
1721
- * - `DATABASE_URL` UNSET → embedded pglite, file-backed at `<dataDir>/pgdata`
1722
- * (local dev + light self-host + tests). Same pg
1723
- * dialect, same migrations, zero external database.
1724
- *
1725
- * `store.ts` is single-sourced across both — the drizzle query-builder API is
1726
- * identical; only this file branches. A `globalThis` guard keeps the handle a
1727
- * true singleton across dev HMR reloads (the Scheduler shares it in-process).
1728
- *
1729
- * The fresh-deploy baseline lives in `packages/server/drizzle`. In
1730
- * the HOSTED tier they're applied out-of-process via `pnpm db:migrate` before
1731
- * `start` (over the DIRECT `:5432` URL); `runMigrations()` also applies them
1732
- * in-process (the pglite tier's only path, plus dev boot + tests).
426
+ * Embedded PGlite (WASM Postgres) database, file-backed at `<dataDir>/pgdata`,
427
+ * plus the Drizzle handle. A `globalThis` guard keeps the handle a singleton
428
+ * across dev HMR reloads (the Scheduler shares it in-process). Exactly one
429
+ * server process may own a data directory.
1733
430
  */
1734
431
  var schema = {
1735
432
  machines,
@@ -1741,62 +438,24 @@ var schema = {
1741
438
  runLeases,
1742
439
  runReportReceipts,
1743
440
  terminalReportIncidents,
1744
- connectKeys,
1745
- user,
1746
- session,
1747
- account,
1748
- verification
441
+ connectKeys
1749
442
  };
1750
443
  var g$1 = globalThis;
1751
- /** Host (host:port, NEVER credentials) of a Postgres URL, for boot logging. */
1752
- function pgHost(url) {
1753
- try {
1754
- return new URL(url).host || "unknown";
1755
- } catch {
1756
- return "unparseable";
1757
- }
1758
- }
1759
444
  function open() {
1760
- if (g$1.__pievoDb && g$1.__pievoClient && g$1.__pievoDriver) return {
445
+ if (g$1.__pievoDb && g$1.__pievoClient) return {
1761
446
  db: g$1.__pievoDb,
1762
- client: g$1.__pievoClient,
1763
- driver: g$1.__pievoDriver
447
+ client: g$1.__pievoClient
1764
448
  };
1765
- const url = databaseUrl();
1766
- if (url) {
1767
- logger.info({
1768
- tier: "postgres",
1769
- host: pgHost(url)
1770
- }, "database tier: postgres");
1771
- if (!/sslmode=/i.test(url)) logger.warn({ host: pgHost(url) }, "DATABASE_URL has no sslmode — the connection may be plaintext; append ?sslmode=require");
1772
- const mode = dbPoolMode();
1773
- const client = src_default(url, poolOptionsFor(url, mode ? mode === "transaction" : isTransactionPooler(url)));
1774
- const db = drizzle(client, { schema });
1775
- g$1.__pievoClient = client;
1776
- g$1.__pievoDb = db;
1777
- g$1.__pievoDriver = "postgres";
1778
- return {
1779
- db,
1780
- client,
1781
- driver: "postgres"
1782
- };
1783
- }
1784
- 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");
1785
449
  const dir = path.join(dataDir(), "pgdata");
1786
- logger.info({
1787
- tier: "pglite",
1788
- pgdata: dir
1789
- }, "database tier: pglite");
450
+ logger.info({ pgdata: dir }, "database: pglite");
1790
451
  fs.mkdirSync(dataDir(), { recursive: true });
1791
452
  const client = new Ve(dir);
1792
- const db = drizzle$1(client, { schema });
453
+ const db = drizzle(client, { schema });
1793
454
  g$1.__pievoClient = client;
1794
455
  g$1.__pievoDb = db;
1795
- g$1.__pievoDriver = "pglite";
1796
456
  return {
1797
457
  db,
1798
- client,
1799
- driver: "pglite"
458
+ client
1800
459
  };
1801
460
  }
1802
461
  var opened = open();
@@ -1807,7 +466,7 @@ opened.client;
1807
466
  * package root (`../../drizzle`). In the BUILT server the module is bundled under
1808
467
  * `.output/server/{_ssr,_libs}` and `copy-pglite-assets.mjs` lands the folder at
1809
468
  * `.output/server/drizzle` (`../drizzle`). Return the first candidate that exists
1810
- * so the embedded pglite tier migrates in-process from either layout.
469
+ * so the database migrates in-process from either layout.
1811
470
  */
1812
471
  function migrationsDir() {
1813
472
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -1817,23 +476,15 @@ function migrationsDir() {
1817
476
  if (fs.existsSync(builtDir)) return builtDir;
1818
477
  return devDir;
1819
478
  }
1820
- /**
1821
- * Apply generated migrations IN-PROCESS (idempotent). This is the PGLITE tier's
1822
- * only migration path (also dev boot + tests). The HOSTED postgres tier is a
1823
- * NO-OP here: `prestart.mjs` (and `pnpm db:migrate`) own hosted migrations, run
1824
- * once out-of-process over the DIRECT (:5432) URL before serving — re-migrating
1825
- * over a second connection on every boot only risks a wedged connection hanging
1826
- * boot and double-running the advisory lock.
1827
- */
479
+ /** Apply generated migrations in-process (idempotent). */
1828
480
  async function runMigrations() {
1829
- if (opened.driver === "postgres") return;
1830
481
  const dir = migrationsDir();
1831
482
  if (!fs.existsSync(dir)) return;
1832
- const { migrate } = await import("../_libs/drizzle-orm+postgres.mjs").then((n) => n.M);
483
+ const { migrate } = await import("../_libs/drizzle-orm.mjs").then((n) => n.E);
1833
484
  await migrate(opened.db, { migrationsFolder: dir });
1834
485
  }
1835
486
  /**
1836
- * Best-effort clean shutdown of the runtime database client (called from the
487
+ * Best-effort clean shutdown of the database client (called from the
1837
488
  * boot AbortController on SIGINT/SIGTERM). PGlite must be closed explicitly:
1838
489
  * after migrations it owns a referenced WASM timer that otherwise keeps Node
1839
490
  * alive until the managed launcher's graceful-stop deadline. Never throws —
@@ -1841,32 +492,90 @@ async function runMigrations() {
1841
492
  */
1842
493
  async function closeClient() {
1843
494
  try {
1844
- if (opened.driver === "postgres") await opened.client.end({ timeout: 5 });
1845
- else await opened.client.close();
495
+ await opened.client.close();
1846
496
  } catch {}
1847
497
  }
1848
498
  /**
1849
- * The GitHub login-gate condition, in one leaf module. `auth.ts` derives its
1850
- * load-time `authEnabled` value from this, and server boot calls it before any
1851
- * migration or scheduler work.
1852
- *
1853
- * The gate is ON exactly when a complete GitHub OAuth app is configured. Open
1854
- * mode requires both values to be absent; a partial configuration fails closed.
1855
- */
1856
- function loginGateEnabled() {
1857
- const hasClientId = !!process.env.GITHUB_CLIENT_ID?.trim();
1858
- 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");
1859
- return hasClientId;
1860
- }
1861
- /**
1862
- * Data-access layer over Drizzle — replaces c0's file-per-loop store. The API is
1863
- * function-style; persistence is relational (loops and runs are separate tables,
1864
- * and `owner: PeerRef` is gone → `userId` + `machineId`).
1865
- *
1866
- * Under Postgres (postgres-js / pglite) the drizzle session is ASYNC, so every
1867
- * DB-touching function returns a Promise. The 5 pure, DB-free helpers stay sync.
1868
- * Using Drizzle (not raw SQL) keeps the dialect swap a swap, not a rewrite.
499
+ * Data-access layer over Drizzle. Loop-locked state transitions live here; every
500
+ * DB-touching function is async, while the pure helpers stay sync.
1869
501
  */
502
+ var store_exports = /* @__PURE__ */ __exportAll({
503
+ TERMINAL_REPORT_GRACE_MS: () => TERMINAL_REPORT_GRACE_MS,
504
+ acknowledgeRetiredReport: () => acknowledgeRetiredReport,
505
+ acknowledgeRetiredTerminalIncident: () => acknowledgeRetiredTerminalIncident,
506
+ advanceDueSchedules: () => advanceDueSchedules,
507
+ artifactFileReferencesHash: () => artifactFileReferencesHash,
508
+ blobExists: () => blobExists,
509
+ blobHashesOlderThan: () => blobHashesOlderThan,
510
+ blobSizes: () => blobSizes,
511
+ buildLoopManifest: () => buildLoopManifest,
512
+ claimReadyRunForMachine: () => claimReadyRunForMachine,
513
+ countRunSnapshots: () => countRunSnapshots,
514
+ countRuns: () => countRuns,
515
+ createLoop: () => createLoop,
516
+ createMachine: () => createMachine,
517
+ deleteBlob: () => deleteBlob,
518
+ enqueueRun: () => enqueueRun,
519
+ expirePendingRun: () => expirePendingRun,
520
+ finalizeRunningRun: () => finalizeRunningRun,
521
+ forceDeleteLoop: () => forceDeleteLoop,
522
+ forceDeleteMachine: () => forceDeleteMachine,
523
+ getArtifactFile: () => getArtifactFile,
524
+ getExactTerminalReportIncident: () => getExactTerminalReportIncident,
525
+ getLoop: () => getLoop,
526
+ getMachine: () => getMachine,
527
+ getReportReceipt: () => getReportReceipt,
528
+ getRun: () => getRun,
529
+ getRunSnapshot: () => getRunSnapshot,
530
+ getTerminalReportIncidents: () => getTerminalReportIncidents,
531
+ hasPendingRun: () => hasPendingRun,
532
+ hasRunningRun: () => hasRunningRun,
533
+ initializeCronCadence: () => initializeCronCadence,
534
+ lastRun: () => lastRun,
535
+ listArtifacts: () => listArtifacts,
536
+ listLoops: () => listLoops,
537
+ listMachines: () => listMachines,
538
+ listRuns: () => listRuns,
539
+ listRunsBefore: () => listRunsBefore,
540
+ liveBlobRefs: () => liveBlobRefs,
541
+ loopIdsWithSnapshots: () => loopIdsWithSnapshots,
542
+ loopsForMachine: () => loopsForMachine,
543
+ machineReferencesBlob: () => machineReferencesBlob,
544
+ newLoopId: () => newLoopId,
545
+ nextCronAt: () => nextCronAt,
546
+ openRuns: () => openRuns,
547
+ pauseLoop: () => pauseLoop,
548
+ pauseLoopState: () => pauseLoopState,
549
+ prevRunSnapshot: () => prevRunSnapshot,
550
+ pruneRunSnapshots: () => pruneRunSnapshots,
551
+ putReportReceiptIfAbsent: () => putReportReceiptIfAbsent,
552
+ putRunSnapshot: () => putRunSnapshot,
553
+ putTerminalReportIncidentIfAbsent: () => putTerminalReportIncidentIfAbsent,
554
+ recentLoopUsage: () => recentLoopUsage,
555
+ reclaimRun: () => reclaimRun,
556
+ reconcileConfiguredArtifacts: () => reconcileConfiguredArtifacts,
557
+ reconcileReclaimedRun: () => reconcileReclaimedRun,
558
+ reconciliationStatesForRuns: () => reconciliationStatesForRuns,
559
+ recordBlob: () => recordBlob,
560
+ recordRunReportOnce: () => recordRunReportOnce,
561
+ refreshRunHeartbeats: () => refreshRunHeartbeats,
562
+ rejectTerminalReport: () => rejectTerminalReport,
563
+ releaseAbsentReconciliations: () => releaseAbsentReconciliations,
564
+ repairTerminalRunLeases: () => repairTerminalRunLeases,
565
+ requestDeleteLoop: () => requestDeleteLoop,
566
+ requestRunCancel: () => requestRunCancel,
567
+ runningRunForLoop: () => runningRunForLoop,
568
+ runningRunsForMachine: () => runningRunsForMachine,
569
+ setMachineOnline: () => setMachineOnline,
570
+ snapshotBlobRefs: () => snapshotBlobRefs,
571
+ startLoop: () => startLoop,
572
+ stopLoop: () => stopLoop,
573
+ terminalIncidentReceiptId: () => terminalIncidentReceiptId,
574
+ tryDeleteLoop: () => tryDeleteLoop,
575
+ updateLoop: () => updateLoop,
576
+ updateMachine: () => updateMachine,
577
+ upsertConfiguredArtifactFile: () => upsertConfiguredArtifactFile
578
+ });
1870
579
  function newLoopId() {
1871
580
  return `loop-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
1872
581
  }
@@ -1897,9 +606,6 @@ function runTimes(at = nowIso$1()) {
1897
606
  async function listLoops() {
1898
607
  return db.select().from(loops);
1899
608
  }
1900
- async function listLoopsForUser(userId) {
1901
- return db.select().from(loops).where(eq(loops.userId, userId));
1902
- }
1903
609
  async function getLoop(id) {
1904
610
  return (await db.select().from(loops).where(eq(loops.id, id)))[0];
1905
611
  }
@@ -1926,12 +632,7 @@ async function createLoop(input) {
1926
632
  updatedAt: ts
1927
633
  };
1928
634
  return db.transaction(async (tx) => {
1929
- const machine = (await tx.select({
1930
- id: machines.id,
1931
- userId: machines.userId
1932
- }).from(machines).where(eq(machines.id, input.machineId)).for("key share"))[0];
1933
- if (!machine) throw new Error(`machine ${input.machineId} does not exist`);
1934
- if (machine.userId !== input.userId) throw new Error(`machine ${input.machineId} is owned by a different user`);
635
+ if (!(await tx.select({ id: machines.id }).from(machines).where(eq(machines.id, input.machineId)).for("key share"))[0]) throw new Error(`machine ${input.machineId} does not exist`);
1935
636
  return (await tx.insert(loops).values(row).returning())[0];
1936
637
  });
1937
638
  }
@@ -2360,6 +1061,43 @@ async function recordRunReportOnce(input) {
2360
1061
  } : { state: "already-reported" };
2361
1062
  });
2362
1063
  }
1064
+ async function finalizeTerminalRunTx(tx, input) {
1065
+ if (input.receipt) await lockReportIdTx(tx, input.receipt.reportId);
1066
+ const current = (await tx.select().from(loops).where(eq(loops.id, input.loopId)).for("update"))[0];
1067
+ if (!current) return void 0;
1068
+ if (input.receipt) {
1069
+ if ((await tx.select({ id: terminalReportIncidents.id }).from(terminalReportIncidents).where(eq(terminalReportIncidents.reportId, input.receipt.reportId)).limit(1))[0]) return void 0;
1070
+ }
1071
+ const leaseState = input.leaseCheck ? await input.leaseCheck(tx) : void 0;
1072
+ if (input.leaseCheck && !leaseState) return void 0;
1073
+ const target = (await tx.select().from(runs).where(and(eq(runs.id, input.runId), eq(runs.loopId, input.loopId), eq(runs.phase, input.runPhase))).limit(1).for("update"))[0];
1074
+ if (!target) return void 0;
1075
+ const runIndex = await ensureRunIndexTx(tx, target);
1076
+ const at = typeof input.runPatch.ts === "string" ? input.runPatch.ts : nowIso$1();
1077
+ const run = (await tx.update(runs).set({
1078
+ ...input.runPatch,
1079
+ runIndex,
1080
+ updatedAt: at
1081
+ }).where(and(eq(runs.id, input.runId), eq(runs.loopId, input.loopId), eq(runs.phase, input.runPhase))).returning())[0];
1082
+ if (!run) return void 0;
1083
+ const lifecycle = leaseState === "reconciliation-only" ? {
1084
+ loop: current,
1085
+ failureStreak: 0,
1086
+ autoPaused: false
1087
+ } : await terminalLifecycleTx(tx, current, run, at, input.loopPatch, input.failureAutopauseStreak);
1088
+ if (input.leaseTokenHash) await tx.delete(runLeases).where(eq(runLeases.runId, input.runId));
1089
+ if (input.receipt) await tx.insert(runReportReceipts).values(input.receipt);
1090
+ return {
1091
+ run,
1092
+ ...lifecycle,
1093
+ ...input.reconciliation ? { reportOnly: leaseState === "reconciliation-only" } : {}
1094
+ };
1095
+ }
1096
+ async function reclaimedLeaseStateTx(tx, loopId, runId, leaseTokenHash) {
1097
+ const leaseNow = nowIso$1();
1098
+ const lease = (await tx.select({ state: runLeases.state }).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];
1099
+ return lease?.state === "terminal-grace" || lease?.state === "reconciliation-only" ? lease.state : void 0;
1100
+ }
2363
1101
  /** Consume a retired tombstone when its reportId is owned by another run.
2364
1102
  * The run/loop may already be deleted, so only the exact incident receipt and
2365
1103
  * lease tombstone participate in this transaction. */
@@ -2471,32 +1209,19 @@ async function rejectTerminalReport(input) {
2471
1209
  * The loop patch is unreachable when cancel/reclaim/another report won. */
2472
1210
  async function finalizeRunningRun(loopId, runId, runPatch, loopPatch = {}, leaseTokenHash, failureAutopauseStreak = 0, receipt) {
2473
1211
  return db.transaction(async (tx) => {
2474
- if (receipt) await lockReportIdTx(tx, receipt.reportId);
2475
- const current = (await tx.select().from(loops).where(eq(loops.id, loopId)).for("update"))[0];
2476
- if (!current) return void 0;
2477
- if (receipt) {
2478
- if ((await tx.select({ id: terminalReportIncidents.id }).from(terminalReportIncidents).where(eq(terminalReportIncidents.reportId, receipt.reportId)).limit(1))[0]) return void 0;
2479
- }
2480
- if (leaseTokenHash) {
2481
- if ((await activeRunForReportTx(tx, loopId, runId, leaseTokenHash)).state !== "active") return void 0;
2482
- }
2483
- 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];
2484
- if (!target) return void 0;
2485
- const runIndex = await ensureRunIndexTx(tx, target);
2486
- const at = typeof runPatch.ts === "string" ? runPatch.ts : nowIso$1();
2487
- const run = (await tx.update(runs).set({
2488
- ...runPatch,
2489
- runIndex,
2490
- updatedAt: at
2491
- }).where(and(eq(runs.id, runId), eq(runs.loopId, loopId), eq(runs.phase, "running"))).returning())[0];
2492
- if (!run) return void 0;
2493
- const lifecycle = await terminalLifecycleTx(tx, current, run, at, loopPatch, failureAutopauseStreak);
2494
- if (leaseTokenHash) await tx.delete(runLeases).where(eq(runLeases.runId, runId));
2495
- if (receipt) await tx.insert(runReportReceipts).values(receipt);
2496
- return {
2497
- run,
2498
- ...lifecycle
2499
- };
1212
+ return finalizeTerminalRunTx(tx, {
1213
+ loopId,
1214
+ runId,
1215
+ runPhase: "running",
1216
+ runPatch,
1217
+ loopPatch,
1218
+ leaseTokenHash,
1219
+ leaseCheck: leaseTokenHash ? async (lockedTx) => {
1220
+ return (await activeRunForReportTx(lockedTx, loopId, runId, leaseTokenHash)).state === "active" ? "active" : void 0;
1221
+ } : void 0,
1222
+ failureAutopauseStreak,
1223
+ receipt
1224
+ });
2500
1225
  });
2501
1226
  }
2502
1227
  /** Sweep reclaim under the same loop lock as claim/report/cancel. Running
@@ -2548,40 +1273,18 @@ async function releaseAbsentReconciliations(machineId, recoverableRunIds, at = n
2548
1273
  * first real failure from a second concurrent one. */
2549
1274
  async function reconcileReclaimedRun(loopId, runId, leaseTokenHash, runPatch, loopPatch = {}, failureAutopauseStreak = 0, receipt) {
2550
1275
  return db.transaction(async (tx) => {
2551
- if (receipt) await lockReportIdTx(tx, receipt.reportId);
2552
- const current = (await tx.select().from(loops).where(eq(loops.id, loopId)).for("update"))[0];
2553
- if (!current) return void 0;
2554
- if (receipt) {
2555
- if ((await tx.select({ id: terminalReportIncidents.id }).from(terminalReportIncidents).where(eq(terminalReportIncidents.reportId, receipt.reportId)).limit(1))[0]) return void 0;
2556
- }
2557
- const leaseNow = nowIso$1();
2558
- const lease = (await tx.select({
2559
- tokenHash: runLeases.tokenHash,
2560
- state: runLeases.state
2561
- }).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];
2562
- if (!lease) return void 0;
2563
- 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];
2564
- if (!target) return void 0;
2565
- const runIndex = await ensureRunIndexTx(tx, target);
2566
- const at = typeof runPatch.ts === "string" ? runPatch.ts : nowIso$1();
2567
- const run = (await tx.update(runs).set({
2568
- ...runPatch,
2569
- runIndex,
2570
- updatedAt: at
2571
- }).where(and(eq(runs.id, runId), eq(runs.loopId, loopId), eq(runs.phase, "error"))).returning())[0];
2572
- if (!run) return void 0;
2573
- const lifecycle = lease.state === "reconciliation-only" ? {
2574
- loop: current,
2575
- failureStreak: 0,
2576
- autoPaused: false
2577
- } : await terminalLifecycleTx(tx, current, run, at, loopPatch, failureAutopauseStreak);
2578
- await tx.delete(runLeases).where(eq(runLeases.runId, runId));
2579
- if (receipt) await tx.insert(runReportReceipts).values(receipt);
2580
- return {
2581
- run,
2582
- ...lifecycle,
2583
- reportOnly: lease.state === "reconciliation-only"
2584
- };
1276
+ return finalizeTerminalRunTx(tx, {
1277
+ loopId,
1278
+ runId,
1279
+ runPhase: "error",
1280
+ runPatch,
1281
+ loopPatch,
1282
+ leaseTokenHash,
1283
+ leaseCheck: (lockedTx) => reclaimedLeaseStateTx(lockedTx, loopId, runId, leaseTokenHash),
1284
+ failureAutopauseStreak,
1285
+ receipt,
1286
+ reconciliation: true
1287
+ });
2585
1288
  });
2586
1289
  }
2587
1290
  var TERMINAL_REPORT_GRACE_MS = 600 * 1e3;
@@ -2784,9 +1487,6 @@ async function expirePendingRun(runId, expected, at, maxLifetimeMs, message) {
2784
1487
  async function listMachines() {
2785
1488
  return db.select().from(machines).orderBy(asc(machines.name), asc(machines.id));
2786
1489
  }
2787
- async function listMachinesForUser(userId) {
2788
- return db.select().from(machines).where(eq(machines.userId, userId)).orderBy(asc(machines.name), asc(machines.id));
2789
- }
2790
1490
  async function getMachine(id) {
2791
1491
  return (await db.select().from(machines).where(eq(machines.id, id)))[0];
2792
1492
  }
@@ -2801,18 +1501,11 @@ async function updateMachine(id, patch) {
2801
1501
  return getMachine(id);
2802
1502
  }
2803
1503
  /** Atomically retire all loop authority, remove server-owned machine data, revoke
2804
- * enrollment, and delete the machine. Auth-mode callers pass `ownerUserId` so
2805
- * ownership is rechecked under the same lock as deletion; open/server callers omit it. */
2806
- async function forceDeleteMachine(id, ownerUserId) {
1504
+ * enrollment, and delete the machine. */
1505
+ async function forceDeleteMachine(id) {
2807
1506
  return db.transaction(async (tx) => {
2808
- const machine = (await tx.select({
2809
- id: machines.id,
2810
- userId: machines.userId
2811
- }).from(machines).where(eq(machines.id, id)).for("update"))[0];
2812
- if (!machine) return { state: "not-found" };
2813
- if (ownerUserId !== void 0 && machine.userId !== ownerUserId) return { state: "forbidden" };
1507
+ if (!(await tx.select({ id: machines.id }).from(machines).where(eq(machines.id, id)).for("update"))[0]) return { state: "not-found" };
2814
1508
  const ownedLoops = await tx.select().from(loops).where(eq(loops.machineId, id)).for("update");
2815
- if (ownerUserId !== void 0 && ownedLoops.some((loop) => loop.userId !== ownerUserId)) return { state: "forbidden" };
2816
1509
  for (const loop of ownedLoops) if (!await forceDeleteLoopTx(tx, loop.id)) throw new Error(`failed to delete loop ${loop.id}`);
2817
1510
  await tx.delete(connectKeys).where(eq(connectKeys.machineId, id));
2818
1511
  await tx.delete(machines).where(eq(machines.id, id));
@@ -2975,7 +1668,7 @@ async function loopIdsWithSnapshots() {
2975
1668
  async function liveBlobRefs() {
2976
1669
  const refs = /* @__PURE__ */ new Set();
2977
1670
  for (const r of await db.selectDistinct({ hash: artifactFiles.hash }).from(artifactFiles).where(isNotNull(artifactFiles.hash))) if (r.hash) refs.add(r.hash);
2978
- 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);
1671
+ for (const hash of await collectSnapshotBlobRefs()) refs.add(hash);
2979
1672
  return refs;
2980
1673
  }
2981
1674
  /** Blob hashes whose metadata row predates `cutoffIso` (GC candidates — the grace
@@ -3001,17 +1694,28 @@ async function artifactFileReferencesHash(hash) {
3001
1694
  * raced the pass), so a snapshot that comes to reference a hash mid-pass is still
3002
1695
  * caught — closing the GC-check-time gap where a snapshot references a hash no live
3003
1696
  * file row does — without paying O(garbage × snapshots). */
3004
- async function snapshotBlobRefs() {
1697
+ async function collectSnapshotBlobRefs() {
3005
1698
  const refs = /* @__PURE__ */ new Set();
3006
1699
  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);
3007
1700
  return refs;
3008
1701
  }
1702
+ async function snapshotBlobRefs() {
1703
+ return collectSnapshotBlobRefs();
1704
+ }
3009
1705
  /** Count of retained run_snapshot rows — the GC's cheap change-detector for deciding
3010
1706
  * whether to rebuild its precomputed snapshotBlobRefs() set mid-pass. */
3011
1707
  async function countRunSnapshots() {
3012
1708
  const r = (await db.select({ n: sql`count(*)` }).from(runSnapshots))[0];
3013
1709
  return Number(r?.n ?? 0);
3014
1710
  }
1711
+ var artifacts_exports = /* @__PURE__ */ __exportAll({
1712
+ BLOB_CAP: () => BLOB_CAP,
1713
+ SYNC_BODY_CAP: () => SYNC_BODY_CAP,
1714
+ isValidHash: () => isValidHash,
1715
+ parseArtifactSyncBody: () => parseArtifactSyncBody,
1716
+ safeRelPath: () => safeRelPath,
1717
+ sha256Buf: () => sha256Buf
1718
+ });
3015
1719
  /** Per-file byte cap. At or under ⇒ bytes sync; over ⇒ metadata-only (no bytes). */
3016
1720
  var BLOB_CAP = 10 * 1024 * 1024;
3017
1721
  /** Hard ceiling on the exact-manifest sync POST body. */
@@ -3099,9 +1803,26 @@ function parseArtifactSyncBody(value) {
3099
1803
  * Leases and connect-key bindings are DURABLE (run_leases / connect_keys
3100
1804
  * tables): they must survive a deploy, or every restart 401s the in-flight
3101
1805
  * runs' callbacks/finalize and prevents a freshly connected machine from
3102
- * enrolling under its owner. The 15-minute `new` idempotency window stays in-process;
1806
+ * enrolling. The 15-minute `new` idempotency window stays in-process;
3103
1807
  * losing it only degrades retry deduplication, never persisted data.
3104
1808
  */
1809
+ var tokens_exports = /* @__PURE__ */ __exportAll({
1810
+ CONNECT_KEY_TTL_MS: () => CONNECT_KEY_TTL_MS,
1811
+ NEW_IDEMPOTENCY_TTL_MS: () => NEW_IDEMPOTENCY_TTL_MS,
1812
+ TERMINAL_GRACE_MS: () => TERMINAL_GRACE_MS,
1813
+ connectKeyValid: () => connectKeyValid,
1814
+ isDeviceTokenShape: () => isDeviceTokenShape,
1815
+ isRunTokenShape: () => isRunTokenShape,
1816
+ machineIdFromToken: () => machineIdFromToken,
1817
+ mintDeviceToken: () => mintDeviceToken,
1818
+ pruneExpiredLeases: () => pruneExpiredLeases,
1819
+ readNewIdempotency: () => readNewIdempotency,
1820
+ recordNewIdempotency: () => recordNewIdempotency,
1821
+ rememberConnectKey: () => rememberConnectKey,
1822
+ resolveLease: () => resolveLease,
1823
+ retireLease: () => retireLease,
1824
+ sha256: () => sha256
1825
+ });
3105
1826
  function sha256(s) {
3106
1827
  return createHash("sha256").update(s).digest("hex");
3107
1828
  }
@@ -3124,13 +1845,12 @@ var CONNECT_KEY_TTL_MS = 1440 * 60 * 1e3;
3124
1845
  function connectKeyFresh(mintedAt, now) {
3125
1846
  return now - Date.parse(mintedAt) <= CONNECT_KEY_TTL_MS;
3126
1847
  }
3127
- /** Bind a freshly minted connect key to its owner and prune expired rows. */
3128
- async function rememberConnectKey(connectKey, binding) {
1848
+ /** Record a freshly minted connect key and prune expired rows. */
1849
+ async function rememberConnectKey(connectKey) {
3129
1850
  const now = /* @__PURE__ */ new Date();
3130
1851
  await db.delete(connectKeys).where(lt(connectKeys.mintedAt, (/* @__PURE__ */ new Date(now.getTime() - CONNECT_KEY_TTL_MS)).toISOString()));
3131
1852
  const row = {
3132
1853
  machineId: machineIdFromToken(connectKey),
3133
- userId: binding.userId,
3134
1854
  mintedAt: now.toISOString()
3135
1855
  };
3136
1856
  await db.insert(connectKeys).values(row).onConflictDoUpdate({
@@ -3138,11 +1858,10 @@ async function rememberConnectKey(connectKey, binding) {
3138
1858
  set: row
3139
1859
  });
3140
1860
  }
3141
- async function readConnectKeyBinding(connectKey, now = Date.now()) {
3142
- if (!connectKey) return void 0;
1861
+ async function connectKeyValid(connectKey, now = Date.now()) {
1862
+ if (!connectKey) return false;
3143
1863
  const row = (await db.select().from(connectKeys).where(eq(connectKeys.machineId, machineIdFromToken(connectKey))))[0];
3144
- if (!row || !connectKeyFresh(row.mintedAt, now)) return void 0;
3145
- return { userId: row.userId };
1864
+ return !!row && connectKeyFresh(row.mintedAt, now);
3146
1865
  }
3147
1866
  /** How long a terminal-grace lease stays alive to accept one late wake-report.
3148
1867
  * A laptop can sleep overnight or across a weekend before the daemon resumes. */
@@ -3198,6 +1917,8 @@ async function pruneExpiredLeases(now = Date.now()) {
3198
1917
  }).where(and(inArray(runLeases.state, ["terminal-grace", "reconciliation-only"]), isNotNull(runLeases.expiresAt), lt(runLeases.expiresAt, new Date(now).toISOString())));
3199
1918
  }
3200
1919
  var newIdempotency = /* @__PURE__ */ new Map();
1920
+ /** Long enough to swallow a timed-out retry without collapsing later creates. */
1921
+ var NEW_IDEMPOTENCY_TTL_MS = 900 * 1e3;
3201
1922
  function pruneNewIdempotency(now) {
3202
1923
  for (const [key, rec] of newIdempotency) if (now - rec.at > 9e5) newIdempotency.delete(key);
3203
1924
  }
@@ -3292,6 +2013,80 @@ function isOutdated(current, latest) {
3292
2013
  }
3293
2014
  return isPre(current) && !isPre(latest);
3294
2015
  }
2016
+ var reserved = new Set(RESERVED_LOOP_TAGS);
2017
+ function normalizeLoopTag(value) {
2018
+ return value.normalize("NFKC").trim().replace(/\s+/gu, " ").toLocaleLowerCase("en-US");
2019
+ }
2020
+ function validateLoopTags(value) {
2021
+ if (value === void 0) return {
2022
+ ok: true,
2023
+ value: []
2024
+ };
2025
+ if (!Array.isArray(value)) return {
2026
+ ok: false,
2027
+ detail: "tags must be an array of strings"
2028
+ };
2029
+ if (value.length > 4) return {
2030
+ ok: false,
2031
+ detail: `tags must contain at most 4 values`
2032
+ };
2033
+ const tags = [];
2034
+ const seen = /* @__PURE__ */ new Set();
2035
+ for (const entry of value) {
2036
+ if (typeof entry !== "string") return {
2037
+ ok: false,
2038
+ detail: "tags must contain only strings"
2039
+ };
2040
+ if (/\p{Cc}|\p{Cf}|\p{Default_Ignorable_Code_Point}/u.test(entry)) return {
2041
+ ok: false,
2042
+ detail: "tags must not contain control or invisible characters"
2043
+ };
2044
+ const tag = normalizeLoopTag(entry);
2045
+ if (!tag) return {
2046
+ ok: false,
2047
+ detail: "tags must not contain empty values"
2048
+ };
2049
+ if ([...tag].length > 64) return {
2050
+ ok: false,
2051
+ detail: `each tag must contain at most 64 characters`
2052
+ };
2053
+ if (reserved.has(tag)) return {
2054
+ ok: false,
2055
+ detail: `reserved tag: ${tag}`
2056
+ };
2057
+ if (seen.has(tag)) return {
2058
+ ok: false,
2059
+ detail: `duplicate tag: ${tag}`
2060
+ };
2061
+ seen.add(tag);
2062
+ tags.push(tag);
2063
+ }
2064
+ return {
2065
+ ok: true,
2066
+ value: [...tags].sort()
2067
+ };
2068
+ }
2069
+ //#endregion
2070
+ //#region node_modules/.nitro/vite/services/ssr/assets/boot-CYRi-wez.js
2071
+ var boot_CYRi_wez_exports = /* @__PURE__ */ __exportAll$1({
2072
+ _: () => validOptionalPollString,
2073
+ a: () => POLL_HTTP_BODY_FIELDS,
2074
+ c: () => MIN_DAEMON_VERSION,
2075
+ d: () => canonicalLoopSummary,
2076
+ f: () => validateLoopEdit,
2077
+ g: () => readJsonBody,
2078
+ h: () => readByteBody,
2079
+ i: () => runDiff_exports,
2080
+ l: () => daemonNeedsUpdate,
2081
+ m: () => WIRE_TEXT_CAP,
2082
+ n: () => ensureServer,
2083
+ o: () => validPollCurrentRun,
2084
+ p: () => MACHINE_BODY_CAP,
2085
+ r: () => getArtifactSync,
2086
+ s: () => validPollWireId,
2087
+ t: () => boot_exports,
2088
+ u: () => canonicalLoopEnvelope
2089
+ });
3295
2090
  /**
3296
2091
  * Machine-route ingress: read + parse a JSON request body under a hard size cap.
3297
2092
  * The gateway's per-field wire caps (WIRE_TEXT_CAP etc.) clip individual strings
@@ -3381,7 +2176,7 @@ function nowIso() {
3381
2176
  /** Strip NUL (U+0000) from a wire string: Postgres text/jsonb columns REJECT the
3382
2177
  * NUL byte (SQLite tolerated it), so a daemon-supplied string carrying one would
3383
2178
  * throw mid-finalize on the DB write. The single sanitizing primitive behind
3384
- * `clipText` and index.ts's `str` - and used directly by `cli.ts`
2179
+ * `clipText` - and used directly by `cli.ts`
3385
2180
  * (parseFlags/validateState, the same one-chokepoint discipline). */
3386
2181
  function stripNul(s) {
3387
2182
  return s.replace(/\u0000/g, "");
@@ -3422,13 +2217,8 @@ async function buildDelivery(loop, queuedRun, runToken, roots) {
3422
2217
  * loop/run). The store ONLY writes/reads bytes — it never executes or interprets
3423
2218
  * them — preserving the server's zero-exec invariant.
3424
2219
  *
3425
- * Three implementations behind one small interface:
3426
- * • LocalBlobStore — durable filesystem default under PIEVO_DATA_DIR/blobs.
3427
- * • R2BlobStore — Cloudflare R2 (S3-compatible) when credentials are configured.
3428
- * • MemoryBlobStore — explicit ephemeral selection or injected test fake.
3429
- *
3430
- * The S3 client is dynamic-imported inside R2BlobStore so tests (and any deploy
3431
- * without R2 creds) never load the AWS SDK, and it stays out of the client bundle.
2220
+ * `LocalBlobStore` is the durable store under `<dataDir>/blobs`;
2221
+ * `MemoryBlobStore` is the injected test fake.
3432
2222
  */
3433
2223
  var log$4 = logger.child({ mod: "blobstore" });
3434
2224
  var SHA256_HEX = /^[a-f0-9]{64}$/;
@@ -3542,132 +2332,15 @@ var LocalBlobStore = class {
3542
2332
  await this.syncDir(path.dirname(target));
3543
2333
  }
3544
2334
  };
3545
- var MemoryBlobStore = class {
3546
- map = /* @__PURE__ */ new Map();
3547
- async has(hash) {
3548
- return this.map.has(hash);
3549
- }
3550
- async put(hash, bytes) {
3551
- this.map.set(hash, Buffer.from(bytes));
3552
- }
3553
- async get(hash) {
3554
- return this.map.get(hash) ?? null;
3555
- }
3556
- async delete(hash) {
3557
- this.map.delete(hash);
3558
- }
3559
- };
3560
- function blobKey(hash) {
3561
- return `blobs/${hash}`;
3562
- }
3563
- var R2BlobStore = class {
3564
- cfg;
3565
- client;
3566
- constructor(cfg) {
3567
- this.cfg = cfg;
3568
- }
3569
- async s3() {
3570
- if (this.client) return this.client;
3571
- const { S3Client } = await import("../_libs/@aws-sdk/client-s3+[...].mjs").then((n) => n.t);
3572
- this.client = new S3Client({
3573
- region: this.cfg.region,
3574
- endpoint: this.cfg.endpoint,
3575
- credentials: {
3576
- accessKeyId: this.cfg.accessKeyId,
3577
- secretAccessKey: this.cfg.secretAccessKey
3578
- },
3579
- forcePathStyle: true
3580
- });
3581
- return this.client;
3582
- }
3583
- async has(hash) {
3584
- const { HeadObjectCommand } = await import("../_libs/@aws-sdk/client-s3+[...].mjs").then((n) => n.t);
3585
- const client = await this.s3();
3586
- try {
3587
- await client.send(new HeadObjectCommand({
3588
- Bucket: this.cfg.bucket,
3589
- Key: blobKey(hash)
3590
- }));
3591
- return true;
3592
- } catch (err) {
3593
- if (isNotFound(err)) return false;
3594
- throw err;
3595
- }
3596
- }
3597
- async put(hash, bytes) {
3598
- const { PutObjectCommand } = await import("../_libs/@aws-sdk/client-s3+[...].mjs").then((n) => n.t);
3599
- await (await this.s3()).send(new PutObjectCommand({
3600
- Bucket: this.cfg.bucket,
3601
- Key: blobKey(hash),
3602
- Body: bytes,
3603
- ContentLength: bytes.length
3604
- }));
3605
- }
3606
- async get(hash) {
3607
- const { GetObjectCommand } = await import("../_libs/@aws-sdk/client-s3+[...].mjs").then((n) => n.t);
3608
- const client = await this.s3();
3609
- try {
3610
- const body = (await client.send(new GetObjectCommand({
3611
- Bucket: this.cfg.bucket,
3612
- Key: blobKey(hash)
3613
- }))).Body;
3614
- if (!body?.transformToByteArray) return null;
3615
- return Buffer.from(await body.transformToByteArray());
3616
- } catch (err) {
3617
- if (isNotFound(err)) return null;
3618
- throw err;
3619
- }
3620
- }
3621
- async delete(hash) {
3622
- const { DeleteObjectCommand } = await import("../_libs/@aws-sdk/client-s3+[...].mjs").then((n) => n.t);
3623
- await (await this.s3()).send(new DeleteObjectCommand({
3624
- Bucket: this.cfg.bucket,
3625
- Key: blobKey(hash)
3626
- }));
3627
- }
3628
- };
3629
- function isNotFound(err) {
3630
- const e = err;
3631
- return e?.name === "NotFound" || e?.name === "NoSuchKey" || e?.$metadata?.httpStatusCode === 404;
3632
- }
3633
- /**
3634
- * The configured blob store: durable local filesystem under PIEVO_DATA_DIR by
3635
- * default; complete R2 credentials preserve the existing R2 selection behavior,
3636
- * while memory requires an explicit selector. Constructed once and shared by every
3637
- * gateway so writes, reads, and GC use the same adapter.
3638
- */
2335
+ /** The one local blob store shared by sync writes, artifact reads, and GC. */
3639
2336
  function createBlobStore() {
3640
- const selection = process.env.PIEVO_BLOB_STORE?.trim().toLowerCase();
3641
- if (selection && ![
3642
- "local",
3643
- "r2",
3644
- "memory"
3645
- ].includes(selection)) throw new Error("PIEVO_BLOB_STORE must be local, r2, or memory");
3646
- if (selection === "memory") {
3647
- log$4.warn("blob store: EPHEMERAL MEMORY explicitly selected; all artifact bytes are lost on restart");
3648
- return new MemoryBlobStore();
3649
- }
3650
- if (selection === "local") {
3651
- const local = new LocalBlobStore();
3652
- log$4.info({ root: local.root }, "blob store: local filesystem (explicit)");
3653
- return local;
3654
- }
3655
- const cfg = r2Config();
3656
- if (selection === "r2" && !cfg) throw new Error("PIEVO_BLOB_STORE=r2 requires complete PIEVO_R2_* configuration");
3657
- if (cfg) {
3658
- log$4.info({
3659
- bucket: cfg.bucket,
3660
- endpoint: cfg.endpoint
3661
- }, "blob store: Cloudflare R2");
3662
- return new R2BlobStore(cfg);
3663
- }
3664
2337
  const local = new LocalBlobStore();
3665
2338
  log$4.info({ root: local.root }, "blob store: local filesystem");
3666
2339
  return local;
3667
2340
  }
3668
2341
  /**
3669
2342
  * Artifact-storage retention + garbage collection — the policy that keeps the
3670
- * local/R2 byte store from growing monotonically.
2343
+ * local byte store from growing monotonically.
3671
2344
  *
3672
2345
  * The artifact pipeline is content-addressed: blob BYTES are keyed by sha256 and
3673
2346
  * deduped across every loop/run. When a path is deleted or its content changes,
@@ -3698,7 +2371,7 @@ var log$3 = logger.child({ mod: "retention" });
3698
2371
  * (most-recent-N). Returns the total number pruned. This is what makes an old
3699
2372
  * snapshot's now-unreferenced blobs collectable by gcBlobs().
3700
2373
  */
3701
- async function pruneSnapshots(keep = snapshotRetention()) {
2374
+ async function pruneSnapshots(keep = 20) {
3702
2375
  let pruned = 0;
3703
2376
  for (const loopId of await loopIdsWithSnapshots()) pruned += await pruneRunSnapshots(loopId, keep);
3704
2377
  return pruned;
@@ -3727,7 +2400,7 @@ async function pruneSnapshots(keep = snapshotRetention()) {
3727
2400
  * so that file re-uploads its bytes on the next sync. We never leave a
3728
2401
  * live blobs row pointing at deleted bytes.
3729
2402
  */
3730
- async function gcBlobs(blobStore, graceMs = blobGcGraceMs()) {
2403
+ async function gcBlobs(blobStore, graceMs = BLOB_GC_GRACE_MS) {
3731
2404
  const refs = await liveBlobRefs();
3732
2405
  const garbage = (await blobHashesOlderThan(new Date(Date.now() - graceMs).toISOString())).filter((h) => !refs.has(h));
3733
2406
  let snapCount = await countRunSnapshots();
@@ -3753,15 +2426,9 @@ async function gcBlobs(blobStore, graceMs = blobGcGraceMs()) {
3753
2426
  }
3754
2427
  return reclaimed;
3755
2428
  }
3756
- /**
3757
- * One full storage-maintenance pass: prune snapshots, then GC the blobs they
3758
- * freed (plus any already-unreferenced). Order matters — pruning first lets the
3759
- * same pass reclaim the blobs it just unpinned. Idempotent and safe with no
3760
- * garbage (returns zeros).
3761
- */
3762
- async function maintainStorage(blobStore) {
3763
- const snapshotsPruned = await pruneSnapshots();
3764
- const blobsReclaimed = await gcBlobs(blobStore);
2429
+ async function maintainStorage(blobStore, options = {}) {
2430
+ const snapshotsPruned = await pruneSnapshots(options.keep);
2431
+ const blobsReclaimed = await gcBlobs(blobStore, options.graceMs);
3765
2432
  if (snapshotsPruned || blobsReclaimed) log$3.info({
3766
2433
  snapshotsPruned,
3767
2434
  blobsReclaimed
@@ -3884,6 +2551,7 @@ function doc(...sections) {
3884
2551
  }
3885
2552
  var LOOP_CONFIG_FIELDS = [
3886
2553
  "name",
2554
+ "tags",
3887
2555
  "schedule",
3888
2556
  "workdir",
3889
2557
  "agent",
@@ -4175,18 +2843,24 @@ function statusDefinitionsFromLoop(loop) {
4175
2843
  };
4176
2844
  }
4177
2845
  function canonicalLoopEnvelope(loop) {
2846
+ return {
2847
+ ...canonicalLoopSummary(loop),
2848
+ prompt: loop.prompt,
2849
+ statusDefinitions: statusDefinitionsFromLoop(loop),
2850
+ artifacts: loop.artifacts,
2851
+ enabled: loop.enabled
2852
+ };
2853
+ }
2854
+ function canonicalLoopSummary(loop) {
4178
2855
  return {
4179
2856
  id: loop.id,
4180
2857
  name: loop.name,
2858
+ tags: loop.tags,
4181
2859
  schedule: scheduleFromLoop(loop),
4182
2860
  workdir: loop.workdir,
4183
2861
  agent: loop.agent,
4184
2862
  model: loop.model ?? null,
4185
- reasoningEffort: loop.reasoningEffort ?? null,
4186
- prompt: loop.prompt,
4187
- statusDefinitions: statusDefinitionsFromLoop(loop),
4188
- artifacts: loop.artifacts,
4189
- enabled: loop.enabled
2863
+ reasoningEffort: loop.reasoningEffort ?? null
4190
2864
  };
4191
2865
  }
4192
2866
  function validateLoopCreate(value) {
@@ -4202,6 +2876,8 @@ function validateLoopCreate(value) {
4202
2876
  };
4203
2877
  const name = requiredText(raw.name, "name");
4204
2878
  if (!name.ok) return name;
2879
+ const tags = validateLoopTags(raw.tags);
2880
+ if (!tags.ok) return tags;
4205
2881
  const workdir = validateWorkdir(raw.workdir);
4206
2882
  if (!workdir.ok) return workdir;
4207
2883
  const prompt = requiredText(raw.prompt, "prompt", true);
@@ -4227,6 +2903,7 @@ function validateLoopCreate(value) {
4227
2903
  };
4228
2904
  const config = {
4229
2905
  name: name.value,
2906
+ tags: tags.value,
4230
2907
  schedule: schedule.value,
4231
2908
  workdir: workdir.value,
4232
2909
  agent,
@@ -4243,6 +2920,7 @@ function validateLoopCreate(value) {
4243
2920
  config,
4244
2921
  row: {
4245
2922
  name: config.name,
2923
+ tags: config.tags,
4246
2924
  ...rowSchedule(config.schedule),
4247
2925
  workdir: config.workdir,
4248
2926
  agent: config.agent,
@@ -4275,6 +2953,11 @@ function validateLoopEdit(loop, value) {
4275
2953
  if (!result.ok) return result;
4276
2954
  update.name = result.value;
4277
2955
  }
2956
+ if (raw.tags !== void 0) {
2957
+ const result = validateLoopTags(raw.tags);
2958
+ if (!result.ok) return result;
2959
+ update.tags = result.value;
2960
+ }
4278
2961
  if (raw.workdir !== void 0) {
4279
2962
  const result = validateWorkdir(raw.workdir);
4280
2963
  if (!result.ok) return result;
@@ -4394,7 +3077,7 @@ var configuredRunTimeoutMs = Number(process.env.PIEVO_RUN_TIMEOUT_MS || 20 * 6e4
4394
3077
  var RUN_TIMEOUT_MS = Number.isFinite(configuredRunTimeoutMs) && configuredRunTimeoutMs > 0 ? configuredRunTimeoutMs : 20 * 6e4;
4395
3078
  /** The ONLY keys an owner `editLoop` patch may touch. A key outside this set is
4396
3079
  * rejected (400) rather than silently ignored, so a `--json` typo fails loudly
4397
- * and identity/ownership columns (id/userId/machineId/timestamps) can never be
3080
+ * and identity columns (id/machineId/timestamps) can never be
4398
3081
  * patched over the device-token edit surface. Exported for `cli.ts`
4399
3082
  * (the `new`/`edit` verb help lists these keys). */
4400
3083
  var EDITABLE_LOOP_FIELDS = LOOP_EDIT_FIELDS;
@@ -4692,7 +3375,7 @@ var MachineGateway = class {
4692
3375
  * grace window + final re-check protect freshly-written/referenced blobs) and
4693
3376
  * idempotent with no garbage. Best-effort — never throws into the caller.
4694
3377
  */
4695
- async maintainStorage() {
3378
+ async maintainStorage(options) {
4696
3379
  if (this.maintenanceRunning) {
4697
3380
  log$2.info("storage maintenance already in progress — skipping this tick");
4698
3381
  return {
@@ -4702,7 +3385,7 @@ var MachineGateway = class {
4702
3385
  }
4703
3386
  this.maintenanceRunning = true;
4704
3387
  try {
4705
- return await maintainStorage(this.blobStore);
3388
+ return await maintainStorage(this.blobStore, options);
4706
3389
  } catch (err) {
4707
3390
  log$2.warn({ err: err instanceof Error ? err.message : String(err) }, "storage maintenance failed");
4708
3391
  return {
@@ -4719,14 +3402,12 @@ var MachineGateway = class {
4719
3402
  const { machineId } = auth;
4720
3403
  let machine = auth.machine;
4721
3404
  if (!machine) {
4722
- const binding = await readConnectKeyBinding(deviceToken);
4723
- if (!binding) return {
3405
+ if (!await connectKeyValid(deviceToken)) return {
4724
3406
  status: 401,
4725
3407
  body: { error: "unknown device token — connect this machine first" }
4726
3408
  };
4727
3409
  machine = await createMachine({
4728
3410
  id: machineId,
4729
- userId: binding.userId,
4730
3411
  name: info?.host || `machine-${machineId.slice(2, 8)}`,
4731
3412
  tokenHash: sha256(deviceToken),
4732
3413
  token: deviceToken,
@@ -4874,7 +3555,7 @@ var MachineGateway = class {
4874
3555
  status: 200,
4875
3556
  body: {
4876
3557
  registered: false,
4877
- claimValid: await readConnectKeyBinding(deviceToken) !== void 0,
3558
+ claimValid: await connectKeyValid(deviceToken),
4878
3559
  online: false,
4879
3560
  name: null,
4880
3561
  lastSeen: null,
@@ -4904,7 +3585,6 @@ var MachineGateway = class {
4904
3585
  const auth = await authenticateDeviceToken(deviceToken);
4905
3586
  if (!auth.ok) return auth.response;
4906
3587
  const { machineId } = auth;
4907
- const machine = auth.machine;
4908
3588
  {
4909
3589
  if (body === null || typeof body !== "object" || Array.isArray(body)) return {
4910
3590
  status: 400,
@@ -4947,7 +3627,6 @@ var MachineGateway = class {
4947
3627
  }
4948
3628
  };
4949
3629
  const loop = await createLoop({
4950
- userId: machine.userId,
4951
3630
  machineId,
4952
3631
  ...row
4953
3632
  });
@@ -5170,6 +3849,51 @@ var MachineGateway = class {
5170
3849
  log$2.info({ runId: lease.runId }, "report: ignored (run was canceled)");
5171
3850
  return response;
5172
3851
  }
3852
+ async reportRaceResponse(runToken, lease, body, payloadDigest) {
3853
+ const raced = await committedReportEvidence(body.reportId, payloadDigest, lease.runId);
3854
+ if (raced.response) return raced.response;
3855
+ if (raced.foreignRun) return this.rejectTerminalAttempt(runToken, lease, body, payloadDigest, "REPORT_CONFLICT", ["reportId was already committed for another run"]);
3856
+ }
3857
+ async resolveLostTerminalRace(runToken, lease, body, payloadDigest, retryReconcile, logMessage) {
3858
+ const raced = await this.reportRaceResponse(runToken, lease, body, payloadDigest);
3859
+ if (raced) return raced;
3860
+ const fresh = await getRun(lease.runId);
3861
+ if (fresh?.phase === "canceled") return this.ignoreCanceledReport(runToken, lease, body);
3862
+ if (fresh?.phase === "done") return {
3863
+ status: 409,
3864
+ body: {
3865
+ error: "run already finalized",
3866
+ code: "REPORT_NOT_FINALIZED",
3867
+ reportId: body.reportId
3868
+ }
3869
+ };
3870
+ const refreshedLease = await resolveLease(runToken);
3871
+ if (refreshedLease?.state === "retired") return this.retiredReport(runToken, body, lease.runId);
3872
+ if (retryReconcile && fresh?.phase === "error" && refreshedLease?.state === "terminal-grace") return this.reconcileReclaimedReport(runToken, refreshedLease, fresh, body);
3873
+ log$2.info({
3874
+ runId: lease.runId,
3875
+ phase: fresh?.phase
3876
+ }, logMessage);
3877
+ return {
3878
+ status: 409,
3879
+ body: {
3880
+ error: "terminal report was not finalized",
3881
+ code: "REPORT_NOT_FINALIZED",
3882
+ reportId: body.reportId
3883
+ }
3884
+ };
3885
+ }
3886
+ async captureRunSnapshot(runId, loopId) {
3887
+ try {
3888
+ await putRunSnapshot(runId, loopId, await buildLoopManifest(loopId));
3889
+ await pruneRunSnapshots(loopId, 20);
3890
+ } catch (err) {
3891
+ log$2.warn({
3892
+ runId,
3893
+ err: err instanceof Error ? err.message : String(err)
3894
+ }, "snapshot capture failed");
3895
+ }
3896
+ }
5173
3897
  /** Reconcile one swept run. The store consumes the terminal-grace lease in the
5174
3898
  * same loop-lock transaction as the error→done/error patch and loop state, so
5175
3899
  * concurrent late reports cannot both win (including error→error). */
@@ -5192,51 +3916,14 @@ var MachineGateway = class {
5192
3916
  ts: nowIso()
5193
3917
  }, loopPatch, AUTOPAUSE_STREAK, receipt);
5194
3918
  } catch (error) {
5195
- const raced = await committedReportEvidence(receipt.reportId, payloadDigest, lease.runId);
5196
- if (raced.response) return raced.response;
5197
- if (raced.foreignRun) return this.rejectTerminalAttempt(runToken, lease, body, payloadDigest, "REPORT_CONFLICT", ["reportId was already committed for another run"]);
3919
+ const raced = await this.reportRaceResponse(runToken, lease, body, payloadDigest);
3920
+ if (raced) return raced;
5198
3921
  throw error;
5199
3922
  }
5200
- if (!reconciled) {
5201
- const raced = await committedReportEvidence(receipt.reportId, payloadDigest, lease.runId);
5202
- if (raced.response) return raced.response;
5203
- if (raced.foreignRun) return this.rejectTerminalAttempt(runToken, lease, body, payloadDigest, "REPORT_CONFLICT", ["reportId was already committed for another run"]);
5204
- const fresh = await getRun(lease.runId);
5205
- if (fresh?.phase === "canceled") return this.ignoreCanceledReport(runToken, lease, body);
5206
- if (fresh?.phase === "done") return {
5207
- status: 409,
5208
- body: {
5209
- error: "run already finalized",
5210
- code: "REPORT_NOT_FINALIZED",
5211
- reportId: body.reportId
5212
- }
5213
- };
5214
- if ((await resolveLease(runToken))?.state === "retired") return this.retiredReport(runToken, body, lease.runId);
5215
- log$2.info({
5216
- runId: lease.runId,
5217
- phase: fresh?.phase
5218
- }, "report: late reconcile lost terminal race");
5219
- return {
5220
- status: 409,
5221
- body: {
5222
- error: "terminal report was not finalized",
5223
- code: "REPORT_NOT_FINALIZED",
5224
- reportId: body.reportId
5225
- }
5226
- };
5227
- }
3923
+ if (!reconciled) return this.resolveLostTerminalRace(runToken, lease, body, payloadDigest, false, "report: late reconcile lost terminal race");
5228
3924
  const deleting = reconciled.loop.deleteRequestedAt != null;
5229
3925
  const reportOnly = reconciled.reportOnly === true;
5230
- if (!deleting && !reportOnly) try {
5231
- await putRunSnapshot(lease.runId, lease.loopId, await buildLoopManifest(lease.loopId));
5232
- await pruneRunSnapshots(lease.loopId, snapshotRetention());
5233
- } catch (err) {
5234
- log$2.warn({
5235
- runId: lease.runId,
5236
- err: err instanceof Error ? err.message : String(err)
5237
- }, "snapshot capture failed");
5238
- }
5239
- reconciled.run;
3926
+ if (!deleting && !reportOnly) await this.captureRunSnapshot(lease.runId, lease.loopId);
5240
3927
  if (!reportOnly) this.scheduler.addLoop(reconciled.loop);
5241
3928
  if (!reportOnly) this.applyAutopauseTimer(lease.loopId, reconciled);
5242
3929
  if (deleting) await tryDeleteLoop(lease.loopId);
@@ -5372,52 +4059,13 @@ var MachineGateway = class {
5372
4059
  ts: nowIso()
5373
4060
  }, loopPatch, sha256(runToken), AUTOPAUSE_STREAK, expected);
5374
4061
  } catch (error) {
5375
- const raced = await committedReportEvidence(reportId, payloadDigest, lease.runId);
5376
- if (raced.response) return raced.response;
5377
- if (raced.foreignRun) return this.rejectTerminalAttempt(runToken, lease, body, payloadDigest, "REPORT_CONFLICT", ["reportId was already committed for another run"]);
4062
+ const raced = await this.reportRaceResponse(runToken, lease, body, payloadDigest);
4063
+ if (raced) return raced;
5378
4064
  throw error;
5379
4065
  }
5380
- if (!terminal) {
5381
- const raced = await committedReportEvidence(reportId, payloadDigest, lease.runId);
5382
- if (raced.response) return raced.response;
5383
- if (raced.foreignRun) return this.rejectTerminalAttempt(runToken, lease, body, payloadDigest, "REPORT_CONFLICT", ["reportId was already committed for another run"]);
5384
- const fresh = await getRun(lease.runId);
5385
- if (fresh?.phase === "canceled") return this.ignoreCanceledReport(runToken, lease, body);
5386
- if (fresh?.phase === "done") return {
5387
- status: 409,
5388
- body: {
5389
- error: "run already finalized",
5390
- code: "REPORT_NOT_FINALIZED",
5391
- reportId
5392
- }
5393
- };
5394
- const refreshedLease = await resolveLease(runToken);
5395
- if (refreshedLease?.state === "retired") return this.retiredReport(runToken, body, lease.runId);
5396
- if (fresh?.phase === "error" && refreshedLease?.state === "terminal-grace") return this.reconcileReclaimedReport(runToken, refreshedLease, fresh, body);
5397
- log$2.info({
5398
- runId: lease.runId,
5399
- phase: fresh?.phase
5400
- }, "report: lost terminal race");
5401
- return {
5402
- status: 409,
5403
- body: {
5404
- error: "terminal report was not finalized",
5405
- code: "REPORT_NOT_FINALIZED",
5406
- reportId
5407
- }
5408
- };
5409
- }
5410
- terminal.run;
4066
+ if (!terminal) return this.resolveLostTerminalRace(runToken, lease, body, payloadDigest, true, "report: lost terminal race");
5411
4067
  const deleting = terminal.loop.deleteRequestedAt != null;
5412
- if (!deleting) try {
5413
- await putRunSnapshot(lease.runId, lease.loopId, await buildLoopManifest(lease.loopId));
5414
- await pruneRunSnapshots(lease.loopId, snapshotRetention());
5415
- } catch (err) {
5416
- log$2.warn({
5417
- runId: lease.runId,
5418
- err: err instanceof Error ? err.message : String(err)
5419
- }, "snapshot capture failed");
5420
- }
4068
+ if (!deleting) await this.captureRunSnapshot(lease.runId, lease.loopId);
5421
4069
  this.scheduler.addLoop(terminal.loop);
5422
4070
  this.applyAutopauseTimer(lease.loopId, terminal);
5423
4071
  log$2.info({
@@ -5689,6 +4337,7 @@ var ArtifactSync = class {
5689
4337
  * process execution, so the server's zero-exec invariant holds. Binary/oversize
5690
4338
  * files emit a size-delta marker only (no inline diff).
5691
4339
  */
4340
+ var runDiff_exports = /* @__PURE__ */ __exportAll({ computeRunDiff: () => computeRunDiff });
5692
4341
  /**
5693
4342
  * Upper bound for computing an inline unified diff. jsdiff is synchronous and
5694
4343
  * roughly O(N*D); on a multi-MB or minified text file it would block the
@@ -6405,7 +5054,6 @@ var CliGateway = class {
6405
5054
  action: "force-delete",
6406
5055
  loopId: loop.id,
6407
5056
  machineId,
6408
- actorUserId: loop.userId,
6409
5057
  machineReachability: reachability
6410
5058
  });
6411
5059
  return {
@@ -6793,6 +5441,7 @@ function renderShowText(loop, env, totalRuns, lastRun, opts = {}) {
6793
5441
  const block = detailBlock("loop", [
6794
5442
  ["id", env.id],
6795
5443
  ["name", env.name],
5444
+ ["tags", { raw: JSON.stringify(env.tags) }],
6796
5445
  ["schedule", { raw: JSON.stringify(env.schedule) }],
6797
5446
  ["workdir", env.workdir],
6798
5447
  ["agent", env.agent],
@@ -7029,87 +5678,6 @@ function msg(err) {
7029
5678
  return err instanceof Error ? err.message : String(err);
7030
5679
  }
7031
5680
  /**
7032
- * DB watchdog — the real auto-recovery backstop for a wedged postgres-js pool.
7033
- *
7034
- * BACKGROUND (2026-07-12 outage, twice in one day): the runtime pool against the
7035
- * Supabase transaction pooler wedged — every connection stuck on a query that
7036
- * never returned, so `/api/health/db`'s `select 1` queued behind them forever and
7037
- * the server stopped sending response headers. Fly's health check went `critical`,
7038
- * which only PULLS the machine from load balancing (a well-known Fly footgun:
7039
- * failing `[http_service.checks]` do NOT restart the VM — only a process EXIT
7040
- * triggers `restart.policy = "on-failure"`). Result: ~9h fully down with no
7041
- * auto-recovery until a manual `fly machine restart`. The pool's own self-healing
7042
- * (`max_lifetime`/`statement_timeout`, `db/index.ts`) can't cover a busy-wedged
7043
- * connection or a dead socket that never errors.
7044
- *
7045
- * This watchdog closes that gap: it pings `select 1` on a fixed cadence under a
7046
- * HARD client-side deadline (a hung ping counts as a failure, since the underlying
7047
- * query can't be canceled). After N consecutive failures it exits the process, so
7048
- * Fly restarts the machine with a fresh pool — turning a multi-hour hang into a
7049
- * ~1-2min self-heal. A single healthy ping resets the streak, so a transient blip
7050
- * never restarts the box.
7051
- *
7052
- * Pure + injectable: `makeDbWatchdog` takes the probe / exit / clock as deps so the
7053
- * decision logic is unit-tested with no real DB, timers, or `process.exit`.
7054
- */
7055
- /** Reject if `p` hasn't settled within `ms`. The underlying promise keeps running
7056
- * (we can't cancel a wedged query) — bounded by the exit after `failureThreshold`. */
7057
- function withDeadline(p, ms, setTimer, clearTimer) {
7058
- return new Promise((resolve, reject) => {
7059
- const h = setTimer(() => reject(/* @__PURE__ */ new Error(`db ping timed out after ${ms}ms`)), ms);
7060
- p.then((v) => {
7061
- clearTimer(h);
7062
- resolve(v);
7063
- }, (e) => {
7064
- clearTimer(h);
7065
- reject(e instanceof Error ? e : new Error(String(e)));
7066
- });
7067
- });
7068
- }
7069
- function makeDbWatchdog(deps) {
7070
- const { probe, exit, timeoutMs, failureThreshold, setTimer = (fn, ms) => setTimeout(fn, ms), clearTimer = (h) => clearTimeout(h), log = logger } = deps;
7071
- let consecutive = 0;
7072
- let running = false;
7073
- let done = false;
7074
- async function tick() {
7075
- if (running || done) return;
7076
- running = true;
7077
- try {
7078
- await withDeadline(Promise.resolve().then(probe), timeoutMs, setTimer, clearTimer);
7079
- consecutive = 0;
7080
- } catch (err) {
7081
- consecutive += 1;
7082
- log.warn({
7083
- consecutive,
7084
- failureThreshold,
7085
- err: String(err)
7086
- }, "db watchdog: liveness ping failed");
7087
- if (consecutive >= failureThreshold) {
7088
- done = true;
7089
- log.error({ consecutive }, "db watchdog: database unreachable for too long — exiting so the platform restarts with a fresh pool");
7090
- exit(1);
7091
- }
7092
- } finally {
7093
- running = false;
7094
- }
7095
- }
7096
- return {
7097
- tick,
7098
- failures: () => consecutive
7099
- };
7100
- }
7101
- /**
7102
- * Start the watchdog on a repeating interval. Returns a stop function (cleared on
7103
- * the boot AbortController). The interval is `unref`'d so it never keeps the
7104
- * process alive on its own.
7105
- */
7106
- function startDbWatchdog(deps) {
7107
- const wd = makeDbWatchdog(deps);
7108
- const handle = setInterval(() => void wd.tick(), deps.intervalMs);
7109
- handle.unref?.();
7110
- return () => clearInterval(handle);
7111
- }
7112
- /**
7113
5681
  * Server boot — idempotent one-time init of the in-process backend: apply
7114
5682
  * migrations, wire the machine gateway's dispatcher into the Scheduler, start
7115
5683
  * the Scheduler + an offline-sweep interval. Guarded on `globalThis` so it
@@ -7117,6 +5685,12 @@ function startDbWatchdog(deps) {
7117
5685
  * from any server-side entry (the standalone machine server / TanStack server
7118
5686
  * fns); the first call boots, the rest share the same instance.
7119
5687
  */
5688
+ var boot_exports = /* @__PURE__ */ __exportAll({
5689
+ ensureServer: () => ensureServer,
5690
+ getArtifactSync: () => getArtifactSync,
5691
+ getCliGateway: () => getCliGateway,
5692
+ getGateway: () => getGateway
5693
+ });
7120
5694
  var g = globalThis;
7121
5695
  function ensureServer() {
7122
5696
  if (g.__pievoBooted) return g.__pievoBooted;
@@ -7128,7 +5702,6 @@ function ensureServer() {
7128
5702
  return p;
7129
5703
  }
7130
5704
  async function boot() {
7131
- loginGateEnabled();
7132
5705
  await runMigrations();
7133
5706
  await repairTerminalRunLeases();
7134
5707
  const abort = new AbortController();
@@ -7149,27 +5722,9 @@ async function boot() {
7149
5722
  const sweep = setInterval(() => void gateway.sweep().catch((err) => logger.error({ err: String(err) }, "sweep tick failed")), ONLINE_TTL_MS);
7150
5723
  sweep.unref?.();
7151
5724
  abort.signal.addEventListener("abort", () => clearInterval(sweep), { once: true });
7152
- const gc = setInterval(() => void gateway.maintainStorage().catch((err) => logger.error({ err: String(err) }, "gc tick failed")), gcIntervalMs());
5725
+ const gc = setInterval(() => void gateway.maintainStorage().catch((err) => logger.error({ err: String(err) }, "gc tick failed")), GC_INTERVAL_MS);
7153
5726
  gc.unref?.();
7154
5727
  abort.signal.addEventListener("abort", () => clearInterval(gc), { once: true });
7155
- if (dbWatchdogEnabled()) {
7156
- const intervalMs = dbWatchdogIntervalMs();
7157
- const timeoutMs = dbWatchdogTimeoutMs();
7158
- const failureThreshold = dbWatchdogFailureThreshold();
7159
- logger.info({
7160
- intervalMs,
7161
- timeoutMs,
7162
- failureThreshold
7163
- }, "db watchdog: armed");
7164
- const stopWatchdog = startDbWatchdog({
7165
- probe: () => db.execute(sql`select 1`),
7166
- exit: (code) => process.exit(code),
7167
- intervalMs,
7168
- timeoutMs,
7169
- failureThreshold
7170
- });
7171
- abort.signal.addEventListener("abort", () => stopWatchdog(), { once: true });
7172
- }
7173
5728
  logger.info("pievo server booted");
7174
5729
  return {
7175
5730
  scheduler,
@@ -7190,4 +5745,4 @@ async function getCliGateway() {
7190
5745
  return (await ensureServer()).cliGateway;
7191
5746
  }
7192
5747
  //#endregion
7193
- export { listLoopsForUser as $, getBaseURL as A, updateMachine as At, getRun as B, defu as C, sha256 as Ct, forceDeleteMachine as D, toKebabCase as Dt, forceDeleteLoop as E, stopLoop as Et, getHost as F, wildcardMatch as Ft, isOutdated as G, hasRunningRun as H, getLoop as I, isSafeUrlScheme as J, isProduction as K, getMachine as L, getCliGateway as M, validPollCurrentRun as Mt, getEnvVar as N, validPollWireId as Nt, getArtifactFile as O, tryDeleteLoop as Ot, getGateway as P, validateLoopEdit as Pt, listLoops as Q, getOrigin as R, db as S, scheduleFromLoop as St, env as T, statusDefinitionsFromLoop as Tt, isDevelopment as U, hasPendingRun as V, isDynamicBaseURLConfig as W, kAPIErrorHeaderSymbol as X, isTest as Y, listArtifacts as Z, countRuns as _, rememberConnectKey as _t, BetterAuthError as a, loginGateEnabled as at, createMachine as b, resolveBaseURL as bt, ENV as c, machinePresence as ct, POLL_HTTP_BODY_FIELDS as d, parseArtifactSyncBody as dt, listMachines as et, SYNC_BODY_CAP as f, pauseLoop as ft, computeRunDiff as g, reconciliationStatesForRuns as gt, capitalizeFirstLetter as h, recentLoopUsage as ht, BLOB_CAP as i, logger as it, getBooleanEnvVar as j, validOptionalPollString as jt, getArtifactSync as k, updateLoop as kt, MACHINE_BODY_CAP as l, mintDeviceToken as lt, betterFetch as m, readJsonBody as mt, APIError$1 as n, listRuns as nt, BetterCallError as o, loopsForMachine as ot, ValidationError$1 as p, readByteBody as pt, isRequestLike as q, BASE_ERROR_CODES as r, listRunsBefore as rt, CODING_AGENTS as s, machineIdFromToken as st, APIError as t, listMachinesForUser as tt, MIN_DAEMON_VERSION as u, normalizePathname as ut, createDefu as v, requestDeleteLoop as vt, ensureServer as w, startLoop as wt, daemonNeedsUpdate as x, safeRelPath as xt, createFetch as y, requestRunCancel as yt, getProtocol as z };
5748
+ export { listMachines as A, reconciliationStatesForRuns as B, getMachine as C, isOutdated as D, hasRunningRun as E, machineIdFromToken as F, startLoop as G, requestRunCancel as H, machinePresence as I, updateLoop as J, stopLoop as K, mintDeviceToken as L, listRunsBefore as M, loopTags_C6vATFCp_exports as N, listArtifacts as O, loopsForMachine as P, pauseLoop as R, getLoop as S, hasPendingRun as T, safeRelPath as U, requestDeleteLoop as V, sha256 as W, validateLoopTags as X, updateMachine as Y, countRuns as _, canonicalLoopEnvelope as a, forceDeleteMachine as b, ensureServer as c, readJsonBody as d, validOptionalPollString as f, CODING_AGENTS as g, validateLoopEdit as h, boot_CYRi_wez_exports as i, listRuns as j, listLoops as k, getArtifactSync as l, validPollWireId as m, MIN_DAEMON_VERSION as n, canonicalLoopSummary as o, validPollCurrentRun as p, tryDeleteLoop as q, POLL_HTTP_BODY_FIELDS as r, daemonNeedsUpdate as s, MACHINE_BODY_CAP as t, readByteBody as u, createMachine as v, getRun as w, getArtifactFile as x, forceDeleteLoop as y, recentLoopUsage as z };