diff --git a/src/backend/clients/database/SqliteDatabaseClient.ts b/src/backend/clients/database/SqliteDatabaseClient.ts
index 6881b0aa4..825ca37e9 100644
--- a/src/backend/clients/database/SqliteDatabaseClient.ts
+++ b/src/backend/clients/database/SqliteDatabaseClient.ts
@@ -78,6 +78,7 @@ const AVAILABLE_MIGRATIONS: [number, string[]][] = [
[43, ['0047_app-url-updates.sql']],
[44, ['0048_old-app-names-unique-tuple.sql']],
[45, ['0049_music-player-pdf-player-updates.sql']],
+ [46, ['0050_add_preamble_version.sql']],
];
export class SqliteDatabaseClient extends AbstractDatabaseClient {
diff --git a/src/backend/clients/database/migrations/mysql/mysql_mig_7.sql b/src/backend/clients/database/migrations/mysql/mysql_mig_7.sql
new file mode 100644
index 000000000..672922b90
--- /dev/null
+++ b/src/backend/clients/database/migrations/mysql/mysql_mig_7.sql
@@ -0,0 +1,18 @@
+-- Copyright (C) 2024-present Puter Technologies Inc.
+--
+-- This file is part of Puter.
+--
+-- Puter is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU Affero General Public License as published
+-- by the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU Affero General Public License for more details.
+--
+-- You should have received a copy of the GNU Affero General Public License
+-- along with this program. If not, see .
+
+ALTER TABLE `subdomains` ADD COLUMN `preamble_version` varchar(64) DEFAULT NULL;
diff --git a/src/backend/clients/database/migrations/sqlite/0050_add_preamble_version.sql b/src/backend/clients/database/migrations/sqlite/0050_add_preamble_version.sql
new file mode 100644
index 000000000..672922b90
--- /dev/null
+++ b/src/backend/clients/database/migrations/sqlite/0050_add_preamble_version.sql
@@ -0,0 +1,18 @@
+-- Copyright (C) 2024-present Puter Technologies Inc.
+--
+-- This file is part of Puter.
+--
+-- Puter is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU Affero General Public License as published
+-- by the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU Affero General Public License for more details.
+--
+-- You should have received a copy of the GNU Affero General Public License
+-- along with this program. If not, see .
+
+ALTER TABLE `subdomains` ADD COLUMN `preamble_version` varchar(64) DEFAULT NULL;
diff --git a/src/backend/drivers/workers/WorkerDriver.ts b/src/backend/drivers/workers/WorkerDriver.ts
index d183c6d5c..e40cba134 100644
--- a/src/backend/drivers/workers/WorkerDriver.ts
+++ b/src/backend/drivers/workers/WorkerDriver.ts
@@ -42,6 +42,7 @@ const WORKER_SUBDOMAIN_PREFIX = 'workers.puter.';
let preamble = '';
let preambleError = false;
let preambleLineCount = 0;
+let preambleVersion: string | null = null;
try {
const preamblePath = path.join(
__dirname,
@@ -50,6 +51,13 @@ try {
console.log('reading: ' + preamblePath);
preamble = readFileSync(preamblePath, 'utf-8');
preambleLineCount = preamble.split('\n').length - 1;
+
+ const versionMatch = /^var __PUTER_PREAMBLE_VERSION__\s*=\s*"([^"]+)"/.exec(
+ preamble,
+ );
+ if (versionMatch) {
+ preambleVersion = versionMatch[1];
+ }
} catch {
console.warn(
'[workers] preamble not built — workers will not have puter.js injected.',
@@ -75,6 +83,10 @@ export class WorkerDriver extends PuterDriver {
#cfBaseUrl = '';
+ static currentPreambleVersion(): string | null {
+ return preambleVersion;
+ }
+
override onServerStart(): void {
const cfg = this.#workerConfig();
if (cfg.ACCOUNTID) {
@@ -199,6 +211,7 @@ export class WorkerDriver extends PuterDriver {
String(existingSub.uuid),
{
root_dir_id: loaded.fsEntry?.sqlId ?? null,
+ preamble_version: preambleVersion,
},
{ userId: actor.user.id },
);
@@ -217,6 +230,7 @@ export class WorkerDriver extends PuterDriver {
subdomain: subdomainName,
rootDirId: loaded.fsEntry?.sqlId,
appOwner: appOwnerId,
+ preambleVersion,
});
}
@@ -604,6 +618,14 @@ export class WorkerDriver extends PuterDriver {
preamble + sourceCode,
)) as { success?: boolean; errors?: unknown[]; url?: string };
+ if (cfResult.success && row.uuid) {
+ await this.stores.subdomain.update(
+ String(row.uuid),
+ { preamble_version: preambleVersion },
+ { userId },
+ );
+ }
+
// Notify the user
await this.#notifyUser(userId, workerName, cfResult);
} catch (err) {
diff --git a/src/backend/stores/subdomain/SubdomainStore.js b/src/backend/stores/subdomain/SubdomainStore.js
index 1c7445fe6..393198a8d 100644
--- a/src/backend/stores/subdomain/SubdomainStore.js
+++ b/src/backend/stores/subdomain/SubdomainStore.js
@@ -182,13 +182,14 @@ export class SubdomainStore extends PuterStore {
// ── Writes ───────────────────────────────────────────────────────
- /** @param {{ userId: number, subdomain: string, rootDirId?: number|null, associatedAppId?: number|null, appOwner?: number|null }} opts */
+ /** @param {{ userId: number, subdomain: string, rootDirId?: number|null, associatedAppId?: number|null, appOwner?: number|null, preambleVersion?: string|null }} opts */
async create({
userId,
subdomain,
rootDirId = null,
associatedAppId = null,
appOwner = null,
+ preambleVersion = null,
}) {
if (!userId || !subdomain) {
throw new Error('create: userId and subdomain are required');
@@ -196,8 +197,8 @@ export class SubdomainStore extends PuterStore {
const uuid = uuidv4();
await this.clients.db.write(
`INSERT INTO \`subdomains\`
- (\`uuid\`, \`subdomain\`, \`user_id\`, \`root_dir_id\`, \`associated_app_id\`, \`app_owner\`)
- VALUES (?, ?, ?, ?, ?, ?)`,
+ (\`uuid\`, \`subdomain\`, \`user_id\`, \`root_dir_id\`, \`associated_app_id\`, \`app_owner\`, \`preamble_version\`)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
uuid,
subdomain,
@@ -205,6 +206,7 @@ export class SubdomainStore extends PuterStore {
rootDirId ?? null,
associatedAppId,
appOwner,
+ preambleVersion,
],
);
@@ -215,6 +217,7 @@ export class SubdomainStore extends PuterStore {
root_dir_id: rootDirId ?? null,
associated_app_id: associatedAppId,
app_owner: appOwner,
+ preamble_version: preambleVersion,
};
await this.#refreshCache(row);
await this.#invalidatePrefixListsForUser(userId);
diff --git a/src/worker/scripts/buildPreamble.mjs b/src/worker/scripts/buildPreamble.mjs
index addf5d9e1..94057dd98 100644
--- a/src/worker/scripts/buildPreamble.mjs
+++ b/src/worker/scripts/buildPreamble.mjs
@@ -1,4 +1,5 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises';
+import { execSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -8,6 +9,16 @@ const templatePath = path.join(workerDir, 'template', 'puter-portable.template')
const outputDir = path.join(workerDir, 'dist');
const outputPath = path.join(outputDir, 'workerPreamble.js');
+// Build a version stamp: puter-js version + short git SHA
+const puterJsPkg = JSON.parse(
+ await readFile(path.resolve(workerDir, '../puter-js/package.json'), 'utf-8'),
+);
+let gitSha = 'unknown';
+try {
+ gitSha = execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim();
+} catch { /* not in a git repo — keep "unknown" */ }
+const preambleVersion = `${puterJsPkg.version}+${gitSha}`;
+
const inlineIncludes = async (filePath) => {
const fileContents = await readFile(filePath, 'utf-8');
const lines = fileContents.split('\n');
@@ -36,5 +47,6 @@ const inlineIncludes = async (filePath) => {
};
await mkdir(outputDir, { recursive: true });
+const versionBanner = `var __PUTER_PREAMBLE_VERSION__ = ${JSON.stringify(preambleVersion)};\n`;
const preambleSource = await inlineIncludes(templatePath);
-await writeFile(outputPath, preambleSource);
+await writeFile(outputPath, versionBanner + preambleSource);