@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{actionUi-D8m1FDHO.js → actionUi-BZrLpgHT.js} +1 -1
- package/.output/public/assets/app-DXLuQNb9.css +2 -0
- package/.output/public/assets/index-Di_wpAtL.js +10 -0
- package/.output/public/assets/loops._loopId-X3ClNPMY.js +67 -0
- package/.output/public/assets/loops._loopId_.runs._runId-ByU2BSyF.js +3 -0
- package/.output/public/assets/preload-helper-hRTYB_ju.js +3 -0
- package/.output/public/assets/{routes-79D939J4.js → routes-BIOJZsig.js} +1 -1
- package/.output/public/assets/routes-K8D5uBiV.js +1 -0
- package/.output/server/_chunks/ssr-renderer.mjs +1 -1
- package/.output/server/_libs/{drizzle-orm+postgres.mjs → drizzle-orm.mjs} +93 -2218
- package/.output/server/_ssr/{MachinesModal-DBIP648Z.mjs → MachinesModal-CmVMec6B.mjs} +52 -46
- package/.output/server/_ssr/{actionUi-C_WswNb1.mjs → actionUi-DE-Pb2tv.mjs} +4 -5
- package/.output/server/_ssr/{artifactFiles-BvVmixrs.mjs → artifactFiles-Di1xDKvX.mjs} +5 -7
- package/.output/server/_ssr/{artifactKind-DRCbYHZU.mjs → artifactKind-DqpmtxDU.mjs} +3 -9
- package/.output/server/_ssr/{boot-U-CFI65B.mjs → boot-CYRi-wez.mjs} +531 -1976
- package/.output/server/_ssr/lifecycleUi-BsbUsi5K.mjs +106 -0
- package/.output/server/_ssr/{loopApi-C4nN0msL.mjs → loopApi-Bez1rHV0.mjs} +50 -65
- package/.output/server/_ssr/{loopProjection-DMSNHux6.mjs → loopProjection-esCjyZJg.mjs} +4 -20
- package/.output/server/_ssr/{loops._loopId-Q4pRYPip.mjs → loops._loopId-KUezNY69.mjs} +133 -24
- package/.output/server/_ssr/{loops._loopId_.runs._runId-DNXVmCt5.mjs → loops._loopId_.runs._runId-BFP9oMrp.mjs} +2 -7
- package/.output/server/_ssr/{loops._loopId_.runs._runId-ty3AhkOi.mjs → loops._loopId_.runs._runId-DOkUEesG.mjs} +18 -33
- package/.output/server/_ssr/{machineFns-CkNms3zA.mjs → machineFns-CniGcY8s.mjs} +13 -49
- package/.output/server/_ssr/networkAccess-CIlK3ION.mjs +30 -0
- package/.output/server/_ssr/router-BFIH2UAE.mjs +362 -0
- package/.output/server/_ssr/routes-BSVBANI3.mjs +12 -0
- package/.output/server/_ssr/{routes-BaanDO41.mjs → routes-DnE7XDe_.mjs} +242 -77
- package/.output/server/_ssr/{routes-B3fulW5I.mjs → routes-ngpX7h8D.mjs} +2 -4
- package/.output/server/_ssr/ssr.mjs +25 -34
- package/.output/server/_ssr/start-B5_9Z5W-.mjs +35 -0
- package/.output/server/{_tanstack-start-manifest_v-DkmxsHDK.mjs → _tanstack-start-manifest_v-DnwAupbZ.mjs} +8 -13
- package/.output/server/drizzle/0000_baseline.sql +6 -97
- package/.output/server/drizzle/meta/_journal.json +2 -16
- package/.output/server/index.mjs +43 -50
- package/drizzle/0000_baseline.sql +6 -97
- package/drizzle/meta/_journal.json +2 -16
- package/package.json +4 -8
- package/scripts/pievo-server.mjs +49 -47
- package/scripts/server-cli-lib.mjs +21 -12
- package/scripts/server-network.mjs +139 -0
- package/.output/public/assets/SignIn-CDCdDD1a.js +0 -1
- package/.output/public/assets/app-PPhgN9iN.css +0 -2
- package/.output/public/assets/index-c0XPWXuz.js +0 -10
- package/.output/public/assets/loops._loopId-D7i_BGiE.js +0 -67
- package/.output/public/assets/loops._loopId_.runs._runId-CksSrEY7.js +0 -3
- package/.output/public/assets/preload-helper-Zcmr31XG.js +0 -5
- package/.output/public/assets/routes-BYgl3Tmv.js +0 -1
- package/.output/server/_libs/@aws-crypto/crc32+[...].mjs +0 -419
- package/.output/server/_libs/@aws-sdk/checksums+[...].mjs +0 -10732
- package/.output/server/_libs/@aws-sdk/client-s3+[...].mjs +0 -9475
- package/.output/server/_libs/@aws-sdk/credential-provider-http+[...].mjs +0 -137
- package/.output/server/_libs/@aws-sdk/credential-provider-ini+[...].mjs +0 -440
- package/.output/server/_libs/@aws-sdk/credential-provider-process+[...].mjs +0 -62
- package/.output/server/_libs/@aws-sdk/credential-provider-sso+[...].mjs +0 -818
- package/.output/server/_libs/@aws-sdk/credential-provider-web-identity+[...].mjs +0 -59
- package/.output/server/_libs/@smithy/credential-provider-imds+[...].mjs +0 -342
- package/.output/server/_libs/aws-crypto__crc32c.mjs +0 -314
- package/.output/server/_libs/aws-sdk__nested-clients.mjs +0 -2671
- package/.output/server/_ssr/SignIn-Bp3uWaCQ.mjs +0 -53
- package/.output/server/_ssr/artifacts-CzDJLaa6.mjs +0 -2
- package/.output/server/_ssr/auth-BV0MxVdo.mjs +0 -38355
- package/.output/server/_ssr/auth-client-RTpMpT4L.mjs +0 -1070
- package/.output/server/_ssr/boot-CsLXrLEa.mjs +0 -2
- package/.output/server/_ssr/bun-sqlite-dialect-BW9W1_Ps-D9KUBJ-L.mjs +0 -162
- package/.output/server/_ssr/core-RrmWoKne.mjs +0 -5
- package/.output/server/_ssr/d1-sqlite-dialect-BLC8LXE6-CVHRzH1n.mjs +0 -112
- package/.output/server/_ssr/db-BTX-xORB.mjs +0 -2
- package/.output/server/_ssr/dist-D7UDVZ5G.mjs +0 -342
- package/.output/server/_ssr/factory-DYFx1fg-.mjs +0 -6903
- package/.output/server/_ssr/kysely-adapter-CwdB7yBh.mjs +0 -2
- package/.output/server/_ssr/kysely-migration-tables-JkVUjPF_-bPAX9LR_.mjs +0 -19
- package/.output/server/_ssr/logger-CwiKSMp6.mjs +0 -2
- package/.output/server/_ssr/node-sqlite-dialect-B_g92oFK.mjs +0 -162
- package/.output/server/_ssr/router-DCNZ2xLm.mjs +0 -543
- package/.output/server/_ssr/routes-8JSF7bX_.mjs +0 -27
- package/.output/server/_ssr/runDiff-D7CxAgYL.mjs +0 -2
- package/.output/server/_ssr/start-4ifArtKx.mjs +0 -4
- package/.output/server/_ssr/store-DSNbXxqz.mjs +0 -2
- package/.output/server/_ssr/tokens-DzUv7CCp.mjs +0 -2
- package/.output/server/drizzle/0001_add_pi_agent.sql +0 -9
- package/.output/server/drizzle/0002_remove_teams.sql +0 -15
- package/.output/server/node_modules/tslib/modules/index.js +0 -70
- package/.output/server/node_modules/tslib/modules/package.json +0 -3
- package/.output/server/node_modules/tslib/package.json +0 -47
- package/.output/server/node_modules/tslib/tslib.js +0 -484
- package/.output/server/package.json +0 -9
- package/drizzle/0001_add_pi_agent.sql +0 -9
- package/drizzle/0002_remove_teams.sql +0 -15
- 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 {
|
|
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/
|
|
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
|
|
735
|
-
|
|
736
|
-
var
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
826
|
-
|
|
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
|
-
*
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
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
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*/
|
|
1342
|
-
var
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
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
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
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
|
-
}
|
|
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
|
-
*
|
|
1715
|
-
*
|
|
1716
|
-
*
|
|
1717
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1845
|
-
else await opened.client.close();
|
|
495
|
+
await opened.client.close();
|
|
1846
496
|
} catch {}
|
|
1847
497
|
}
|
|
1848
498
|
/**
|
|
1849
|
-
*
|
|
1850
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
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
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
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.
|
|
2805
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
3128
|
-
async function rememberConnectKey(connectKey
|
|
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
|
|
3142
|
-
if (!connectKey) return
|
|
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
|
-
|
|
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`
|
|
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
|
-
*
|
|
3426
|
-
*
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
3758
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
5196
|
-
if (raced
|
|
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)
|
|
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
|
|
5376
|
-
if (raced
|
|
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)
|
|
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")),
|
|
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 {
|
|
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 };
|