@backifydev/cli 0.3.8 → 0.3.9
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/README.md +3 -1
- package/dist/index.js +1461 -1204
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4598,9 +4598,9 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
4598
4598
|
default: {
|
|
4599
4599
|
const message = `Unexpected token in block scalar header: ${token.type}`;
|
|
4600
4600
|
onError(token, "UNEXPECTED_TOKEN", message);
|
|
4601
|
-
const
|
|
4602
|
-
if (
|
|
4603
|
-
length +=
|
|
4601
|
+
const ts3 = token.source;
|
|
4602
|
+
if (ts3 && typeof ts3 === "string")
|
|
4603
|
+
length += ts3.length;
|
|
4604
4604
|
}
|
|
4605
4605
|
}
|
|
4606
4606
|
}
|
|
@@ -4910,9 +4910,9 @@ var require_compose_scalar = __commonJS({
|
|
|
4910
4910
|
if (schema.compat) {
|
|
4911
4911
|
const compat = schema.compat.find((tag2) => tag2.default && tag2.test?.test(value)) ?? schema[identity.SCALAR];
|
|
4912
4912
|
if (tag.tag !== compat.tag) {
|
|
4913
|
-
const
|
|
4913
|
+
const ts3 = directives.tagString(tag.tag);
|
|
4914
4914
|
const cs = directives.tagString(compat.tag);
|
|
4915
|
-
const msg = `Value may be parsed as either ${
|
|
4915
|
+
const msg = `Value may be parsed as either ${ts3} or ${cs}`;
|
|
4916
4916
|
onError(token, "TAG_RESOLVE_FAILED", msg, true);
|
|
4917
4917
|
}
|
|
4918
4918
|
}
|
|
@@ -7812,7 +7812,7 @@ var require_luxon = __commonJS({
|
|
|
7812
7812
|
* @param {string} opts.locale - What locale to return the offset name in.
|
|
7813
7813
|
* @return {string}
|
|
7814
7814
|
*/
|
|
7815
|
-
offsetName(
|
|
7815
|
+
offsetName(ts3, opts) {
|
|
7816
7816
|
throw new ZoneIsAbstractError();
|
|
7817
7817
|
}
|
|
7818
7818
|
/**
|
|
@@ -7823,7 +7823,7 @@ var require_luxon = __commonJS({
|
|
|
7823
7823
|
* Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
|
|
7824
7824
|
* @return {string}
|
|
7825
7825
|
*/
|
|
7826
|
-
formatOffset(
|
|
7826
|
+
formatOffset(ts3, format) {
|
|
7827
7827
|
throw new ZoneIsAbstractError();
|
|
7828
7828
|
}
|
|
7829
7829
|
/**
|
|
@@ -7832,7 +7832,7 @@ var require_luxon = __commonJS({
|
|
|
7832
7832
|
* @param {number} ts - Epoch milliseconds for which to compute the offset
|
|
7833
7833
|
* @return {number}
|
|
7834
7834
|
*/
|
|
7835
|
-
offset(
|
|
7835
|
+
offset(ts3) {
|
|
7836
7836
|
throw new ZoneIsAbstractError();
|
|
7837
7837
|
}
|
|
7838
7838
|
/**
|
|
@@ -7878,19 +7878,19 @@ var require_luxon = __commonJS({
|
|
|
7878
7878
|
return false;
|
|
7879
7879
|
}
|
|
7880
7880
|
/** @override **/
|
|
7881
|
-
offsetName(
|
|
7881
|
+
offsetName(ts3, {
|
|
7882
7882
|
format,
|
|
7883
7883
|
locale
|
|
7884
7884
|
}) {
|
|
7885
|
-
return parseZoneInfo(
|
|
7885
|
+
return parseZoneInfo(ts3, format, locale);
|
|
7886
7886
|
}
|
|
7887
7887
|
/** @override **/
|
|
7888
|
-
formatOffset(
|
|
7889
|
-
return formatOffset(this.offset(
|
|
7888
|
+
formatOffset(ts3, format) {
|
|
7889
|
+
return formatOffset(this.offset(ts3), format);
|
|
7890
7890
|
}
|
|
7891
7891
|
/** @override **/
|
|
7892
|
-
offset(
|
|
7893
|
-
return -new Date(
|
|
7892
|
+
offset(ts3) {
|
|
7893
|
+
return -new Date(ts3).getTimezoneOffset();
|
|
7894
7894
|
}
|
|
7895
7895
|
/** @override **/
|
|
7896
7896
|
equals(otherZone) {
|
|
@@ -8042,11 +8042,11 @@ var require_luxon = __commonJS({
|
|
|
8042
8042
|
* @param {string} opts.locale - What locale to return the offset name in.
|
|
8043
8043
|
* @return {string}
|
|
8044
8044
|
*/
|
|
8045
|
-
offsetName(
|
|
8045
|
+
offsetName(ts3, {
|
|
8046
8046
|
format,
|
|
8047
8047
|
locale
|
|
8048
8048
|
}) {
|
|
8049
|
-
return parseZoneInfo(
|
|
8049
|
+
return parseZoneInfo(ts3, format, locale, this.name);
|
|
8050
8050
|
}
|
|
8051
8051
|
/**
|
|
8052
8052
|
* Returns the offset's value as a string
|
|
@@ -8056,8 +8056,8 @@ var require_luxon = __commonJS({
|
|
|
8056
8056
|
* Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
|
|
8057
8057
|
* @return {string}
|
|
8058
8058
|
*/
|
|
8059
|
-
formatOffset(
|
|
8060
|
-
return formatOffset(this.offset(
|
|
8059
|
+
formatOffset(ts3, format) {
|
|
8060
|
+
return formatOffset(this.offset(ts3), format);
|
|
8061
8061
|
}
|
|
8062
8062
|
/**
|
|
8063
8063
|
* Return the offset in minutes for this zone at the specified timestamp.
|
|
@@ -8065,9 +8065,9 @@ var require_luxon = __commonJS({
|
|
|
8065
8065
|
* @param {number} ts - Epoch milliseconds for which to compute the offset
|
|
8066
8066
|
* @return {number}
|
|
8067
8067
|
*/
|
|
8068
|
-
offset(
|
|
8068
|
+
offset(ts3) {
|
|
8069
8069
|
if (!this.valid) return NaN;
|
|
8070
|
-
const date = new Date(
|
|
8070
|
+
const date = new Date(ts3);
|
|
8071
8071
|
if (isNaN(date)) return NaN;
|
|
8072
8072
|
const dtf = makeDTF(this.name);
|
|
8073
8073
|
let [year, month, day, adOrBc, hour, minute, second] = dtf.formatToParts ? partsOffset(dtf, date) : hackyOffset(dtf, date);
|
|
@@ -8654,7 +8654,7 @@ var require_luxon = __commonJS({
|
|
|
8654
8654
|
* Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
|
|
8655
8655
|
* @return {string}
|
|
8656
8656
|
*/
|
|
8657
|
-
formatOffset(
|
|
8657
|
+
formatOffset(ts3, format) {
|
|
8658
8658
|
return formatOffset(this.fixed, format);
|
|
8659
8659
|
}
|
|
8660
8660
|
/**
|
|
@@ -9340,8 +9340,8 @@ var require_luxon = __commonJS({
|
|
|
9340
9340
|
return year;
|
|
9341
9341
|
} else return year > Settings.twoDigitCutoffYear ? 1900 + year : 2e3 + year;
|
|
9342
9342
|
}
|
|
9343
|
-
function parseZoneInfo(
|
|
9344
|
-
const date = new Date(
|
|
9343
|
+
function parseZoneInfo(ts3, offsetFormat, locale, timeZone = null) {
|
|
9344
|
+
const date = new Date(ts3), intlOpts = {
|
|
9345
9345
|
hourCycle: "h23",
|
|
9346
9346
|
year: "numeric",
|
|
9347
9347
|
month: "2-digit",
|
|
@@ -12218,9 +12218,9 @@ var require_luxon = __commonJS({
|
|
|
12218
12218
|
}
|
|
12219
12219
|
return [localTS - Math.min(o2, o3) * 60 * 1e3, Math.max(o2, o3)];
|
|
12220
12220
|
}
|
|
12221
|
-
function tsToObj(
|
|
12222
|
-
|
|
12223
|
-
const d = new Date(
|
|
12221
|
+
function tsToObj(ts3, offset2) {
|
|
12222
|
+
ts3 += offset2 * 60 * 1e3;
|
|
12223
|
+
const d = new Date(ts3);
|
|
12224
12224
|
return {
|
|
12225
12225
|
year: d.getUTCFullYear(),
|
|
12226
12226
|
month: d.getUTCMonth() + 1,
|
|
@@ -12251,13 +12251,13 @@ var require_luxon = __commonJS({
|
|
|
12251
12251
|
seconds: dur.seconds,
|
|
12252
12252
|
milliseconds: dur.milliseconds
|
|
12253
12253
|
}).as("milliseconds"), localTS = objToLocalTS(c);
|
|
12254
|
-
let [
|
|
12254
|
+
let [ts3, o] = fixOffset(localTS, oPre, inst.zone);
|
|
12255
12255
|
if (millisToAdd !== 0) {
|
|
12256
|
-
|
|
12257
|
-
o = inst.zone.offset(
|
|
12256
|
+
ts3 += millisToAdd;
|
|
12257
|
+
o = inst.zone.offset(ts3);
|
|
12258
12258
|
}
|
|
12259
12259
|
return {
|
|
12260
|
-
ts:
|
|
12260
|
+
ts: ts3,
|
|
12261
12261
|
o
|
|
12262
12262
|
};
|
|
12263
12263
|
}
|
|
@@ -12444,7 +12444,7 @@ var require_luxon = __commonJS({
|
|
|
12444
12444
|
return DateTime.invalid(unsupportedZone(zone));
|
|
12445
12445
|
}
|
|
12446
12446
|
const loc = Locale.fromObject(opts);
|
|
12447
|
-
let
|
|
12447
|
+
let ts3, o;
|
|
12448
12448
|
if (!isUndefined(obj.year)) {
|
|
12449
12449
|
for (const u of orderedUnits) {
|
|
12450
12450
|
if (isUndefined(obj[u])) {
|
|
@@ -12456,12 +12456,12 @@ var require_luxon = __commonJS({
|
|
|
12456
12456
|
return DateTime.invalid(invalid2);
|
|
12457
12457
|
}
|
|
12458
12458
|
const offsetProvis = guessOffsetForZone(zone);
|
|
12459
|
-
[
|
|
12459
|
+
[ts3, o] = objToTS(obj, offsetProvis, zone);
|
|
12460
12460
|
} else {
|
|
12461
|
-
|
|
12461
|
+
ts3 = Settings.now();
|
|
12462
12462
|
}
|
|
12463
12463
|
return new DateTime({
|
|
12464
|
-
ts:
|
|
12464
|
+
ts: ts3,
|
|
12465
12465
|
zone,
|
|
12466
12466
|
loc,
|
|
12467
12467
|
o
|
|
@@ -12624,8 +12624,8 @@ var require_luxon = __commonJS({
|
|
|
12624
12624
|
* @return {DateTime}
|
|
12625
12625
|
*/
|
|
12626
12626
|
static fromJSDate(date, options = {}) {
|
|
12627
|
-
const
|
|
12628
|
-
if (Number.isNaN(
|
|
12627
|
+
const ts3 = isDate(date) ? date.valueOf() : NaN;
|
|
12628
|
+
if (Number.isNaN(ts3)) {
|
|
12629
12629
|
return _DateTime.invalid("invalid input");
|
|
12630
12630
|
}
|
|
12631
12631
|
const zoneToUse = normalizeZone(options.zone, Settings.defaultZone);
|
|
@@ -12633,7 +12633,7 @@ var require_luxon = __commonJS({
|
|
|
12633
12633
|
return _DateTime.invalid(unsupportedZone(zoneToUse));
|
|
12634
12634
|
}
|
|
12635
12635
|
return new _DateTime({
|
|
12636
|
-
ts:
|
|
12636
|
+
ts: ts3,
|
|
12637
12637
|
zone: zoneToUse,
|
|
12638
12638
|
loc: Locale.fromObject(options)
|
|
12639
12639
|
});
|
|
@@ -13287,14 +13287,14 @@ var require_luxon = __commonJS({
|
|
|
13287
13287
|
return [this];
|
|
13288
13288
|
}
|
|
13289
13289
|
const ts1 = localTS - o1 * minuteMs;
|
|
13290
|
-
const
|
|
13290
|
+
const ts22 = localTS - o2 * minuteMs;
|
|
13291
13291
|
const c1 = tsToObj(ts1, o1);
|
|
13292
|
-
const c2 = tsToObj(
|
|
13292
|
+
const c2 = tsToObj(ts22, o2);
|
|
13293
13293
|
if (c1.hour === c2.hour && c1.minute === c2.minute && c1.second === c2.second && c1.millisecond === c2.millisecond) {
|
|
13294
13294
|
return [clone(this, {
|
|
13295
13295
|
ts: ts1
|
|
13296
13296
|
}), clone(this, {
|
|
13297
|
-
ts:
|
|
13297
|
+
ts: ts22
|
|
13298
13298
|
})];
|
|
13299
13299
|
}
|
|
13300
13300
|
return [this];
|
|
@@ -13493,9 +13493,9 @@ var require_luxon = __commonJS({
|
|
|
13493
13493
|
mixed.day = Math.min(daysInMonth(mixed.year, mixed.month), mixed.day);
|
|
13494
13494
|
}
|
|
13495
13495
|
}
|
|
13496
|
-
const [
|
|
13496
|
+
const [ts3, o] = objToTS(mixed, this.o, this.zone);
|
|
13497
13497
|
return clone(this, {
|
|
13498
|
-
ts:
|
|
13498
|
+
ts: ts3,
|
|
13499
13499
|
o
|
|
13500
13500
|
});
|
|
13501
13501
|
}
|
|
@@ -17666,7 +17666,7 @@ import { hostname, platform, release, arch } from "node:os";
|
|
|
17666
17666
|
// package.json
|
|
17667
17667
|
var package_default = {
|
|
17668
17668
|
name: "@backifydev/cli",
|
|
17669
|
-
version: "0.3.
|
|
17669
|
+
version: "0.3.9",
|
|
17670
17670
|
description: "Command-line setup and diagnostics for Backify applications",
|
|
17671
17671
|
type: "module",
|
|
17672
17672
|
bin: {
|
|
@@ -17725,154 +17725,20 @@ async function registerCliSession(client, applicationId, apiUrl) {
|
|
|
17725
17725
|
return { ...session, apiUrl };
|
|
17726
17726
|
}
|
|
17727
17727
|
|
|
17728
|
-
// src/
|
|
17729
|
-
import {
|
|
17730
|
-
import {
|
|
17731
|
-
import { dirname as dirname2, join, resolve as resolve2, sep } from "node:path";
|
|
17732
|
-
import { homedir } from "node:os";
|
|
17733
|
-
import { spawnSync } from "node:child_process";
|
|
17734
|
-
|
|
17735
|
-
// src/local-state.ts
|
|
17736
|
-
import { renameSync } from "node:fs";
|
|
17737
|
-
function replaceLocalFile(source, destination, rename, platform2, wait) {
|
|
17738
|
-
for (let attempt = 0; ; attempt++) {
|
|
17739
|
-
try {
|
|
17740
|
-
rename(source, destination);
|
|
17741
|
-
return;
|
|
17742
|
-
} catch (error) {
|
|
17743
|
-
if (platform2 !== "win32" || attempt >= 4 || !(error instanceof Error) || !("code" in error) || !["EPERM", "EACCES", "EBUSY"].includes(String(error.code))) throw error;
|
|
17744
|
-
wait(10 * 2 ** attempt);
|
|
17745
|
-
}
|
|
17746
|
-
}
|
|
17747
|
-
}
|
|
17748
|
-
function renameLocalFile(source, destination) {
|
|
17749
|
-
replaceLocalFile(source, destination, renameSync, process.platform, (milliseconds) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds));
|
|
17750
|
-
}
|
|
17751
|
-
|
|
17752
|
-
// src/local-runtime.ts
|
|
17753
|
-
var maxArchiveBytes = 256 * 1024 * 1024;
|
|
17754
|
-
var maxExpandedBytes = 512 * 1024 * 1024;
|
|
17755
|
-
var hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
17756
|
-
function runtimeIdentifier(platform2 = process.platform, architecture = process.arch) {
|
|
17757
|
-
const system = { win32: "win", darwin: "osx", linux: "linux" }[platform2];
|
|
17758
|
-
if (!system || !["x64", "arm64"].includes(architecture)) throw new Error("Local runtime supports Windows, macOS and Linux on x64/arm64");
|
|
17759
|
-
return system + "-" + architecture;
|
|
17760
|
-
}
|
|
17761
|
-
function checkedPath(root, filename2) {
|
|
17762
|
-
const target = resolve2(root, filename2);
|
|
17763
|
-
if (target !== resolve2(root) && !target.startsWith(resolve2(root) + sep)) throw new Error("Runtime path escapes its installation");
|
|
17764
|
-
let current = target;
|
|
17765
|
-
while (true) {
|
|
17766
|
-
if (existsSync2(current) && lstatSync(current).isSymbolicLink()) throw new Error("Runtime installation must not follow links");
|
|
17767
|
-
if (dirname2(current) === current) break;
|
|
17768
|
-
current = dirname2(current);
|
|
17769
|
-
}
|
|
17770
|
-
return target;
|
|
17771
|
-
}
|
|
17772
|
-
function verifyInstallationFiles(root, expected, directory2 = root) {
|
|
17773
|
-
for (const entry of readdirSync(directory2, { withFileTypes: true })) {
|
|
17774
|
-
const path = checkedPath(root, join(directory2, entry.name));
|
|
17775
|
-
if (entry.isDirectory()) verifyInstallationFiles(root, expected, path);
|
|
17776
|
-
else if (!entry.isFile() || !Object.hasOwn(expected, path.slice(root.length + 1).split(sep).join("/"))) throw new Error("Installed runtime contains unexpected files; reinstall a reviewed version");
|
|
17777
|
-
}
|
|
17778
|
-
}
|
|
17779
|
-
function installedLocalRuntime(base = join(homedir(), ".backify")) {
|
|
17780
|
-
const pointer = checkedPath(base, "runtime.json");
|
|
17781
|
-
if (!existsSync2(pointer)) return void 0;
|
|
17782
|
-
const installed = JSON.parse(readFileSync(pointer, "utf8"));
|
|
17783
|
-
if (!/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(installed.version) || installed.rid !== runtimeIdentifier() || !installed.files || Object.keys(installed.files).length > 3e3) throw new Error("Invalid local runtime installation receipt");
|
|
17784
|
-
const root = checkedPath(base, join("runtimes", installed.version, installed.rid));
|
|
17785
|
-
verifyInstallationFiles(root, installed.files);
|
|
17786
|
-
for (const [filename2, digest4] of Object.entries(installed.files)) {
|
|
17787
|
-
const path = checkedPath(root, filename2);
|
|
17788
|
-
if (!/^[a-f0-9]{64}$/.test(digest4) || !lstatSync(path).isFile() || lstatSync(path).size > maxArchiveBytes || hash(readFileSync(path)) !== digest4) throw new Error("Installed runtime bytes changed; reinstall a reviewed version");
|
|
17789
|
-
}
|
|
17790
|
-
const executableName = installed.rid.startsWith("win-") ? "Backify.Hcr.exe" : "Backify.Hcr";
|
|
17791
|
-
if (!installed.files["Backify.Hcr.dll"] || !installed.files[executableName]) throw new Error("Runtime archive requires the self-contained HCR executable and DLL");
|
|
17792
|
-
return { path: join(root, "Backify.Hcr.dll"), executable: join(root, executableName), version: installed.version, rid: installed.rid };
|
|
17793
|
-
}
|
|
17794
|
-
function installLocalRuntime(archive, options) {
|
|
17795
|
-
if (!/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(options.version) || !/^[a-f0-9]{64}$/.test(options.sha256)) throw new Error("Provide a release version vX.Y.Z and an independently reviewed SHA-256");
|
|
17796
|
-
if (archive.byteLength > maxArchiveBytes || hash(archive) !== options.sha256) throw new Error("Runtime archive digest does not match the reviewed SHA-256");
|
|
17797
|
-
let expanded = 0, count = 0;
|
|
17798
|
-
const files = unzipSync(archive, { filter: (file) => {
|
|
17799
|
-
if (++count > 3e3 || (expanded += file.originalSize) > maxExpandedBytes || file.originalSize > maxArchiveBytes) throw new Error("Runtime archive exceeds extraction limits");
|
|
17800
|
-
if (!file.name || file.name.includes("\\") || file.name.startsWith("/") || file.name.includes(":") || file.name.split("/").some((part) => part === "." || part === "..")) throw new Error("Runtime archive contains an unsafe path");
|
|
17801
|
-
return !file.name.endsWith("/");
|
|
17802
|
-
} });
|
|
17803
|
-
const rid = runtimeIdentifier();
|
|
17804
|
-
const executable = rid.startsWith("win-") ? "Backify.Hcr.exe" : "Backify.Hcr";
|
|
17805
|
-
if (!files["Backify.Hcr.dll"] || !files[executable] || !files["sql/events-v1.sql"] || !files["sql/jobs-v1.sql"]) throw new Error("Runtime archive is missing required host assets");
|
|
17806
|
-
const base = options.base ?? join(homedir(), ".backify");
|
|
17807
|
-
const parent = checkedPath(base, join("runtimes", options.version));
|
|
17808
|
-
mkdirSync2(parent, { recursive: true, mode: 448 });
|
|
17809
|
-
const destination = checkedPath(parent, rid);
|
|
17810
|
-
const receipt = { version: options.version, rid, files: Object.fromEntries(Object.entries(files).map(([name2, bytes]) => [name2, hash(bytes)])) };
|
|
17811
|
-
if (!existsSync2(destination)) {
|
|
17812
|
-
const staging = checkedPath(parent, ".install-" + randomUUID());
|
|
17813
|
-
mkdirSync2(staging, { mode: 448 });
|
|
17814
|
-
for (const [name2, bytes] of Object.entries(files)) {
|
|
17815
|
-
const filename2 = checkedPath(staging, name2);
|
|
17816
|
-
mkdirSync2(dirname2(filename2), { recursive: true, mode: 448 });
|
|
17817
|
-
writeFileSync2(filename2, bytes, { flag: "wx", mode: name2 === executable ? 448 : 384 });
|
|
17818
|
-
}
|
|
17819
|
-
if (process.platform !== "win32") chmodSync(join(staging, executable), 448);
|
|
17820
|
-
renameLocalFile(staging, destination);
|
|
17821
|
-
} else {
|
|
17822
|
-
verifyInstallationFiles(destination, receipt.files);
|
|
17823
|
-
for (const [name2, digest4] of Object.entries(receipt.files)) if (hash(readFileSync(checkedPath(destination, name2))) !== digest4) throw new Error("Runtime version already exists with different bytes; never overwrite an installation");
|
|
17824
|
-
}
|
|
17825
|
-
const pointer = checkedPath(base, "runtime.json");
|
|
17826
|
-
const temporary = checkedPath(base, ".runtime-" + randomUUID() + ".json");
|
|
17827
|
-
writeFileSync2(temporary, JSON.stringify(receipt), { flag: "wx", mode: 384 });
|
|
17828
|
-
renameLocalFile(temporary, pointer);
|
|
17829
|
-
return installedLocalRuntime(base);
|
|
17830
|
-
}
|
|
17831
|
-
async function downloadLocalRuntime(version) {
|
|
17832
|
-
if (!/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(version)) throw new Error("Provide a release version vX.Y.Z");
|
|
17833
|
-
let url = new URL("https://github.com/frejnorling/backify/releases/download/runtime-" + version + "/backify-hcr-" + runtimeIdentifier() + ".zip");
|
|
17834
|
-
for (let redirects = 0; redirects < 5; redirects++) {
|
|
17835
|
-
if (url.protocol !== "https:" || !["github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com"].includes(url.hostname) || url.username || url.password) throw new Error("Runtime download left the approved release hosts");
|
|
17836
|
-
const response = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(6e4) });
|
|
17837
|
-
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
17838
|
-
url = new URL(response.headers.get("location") ?? "", url);
|
|
17839
|
-
continue;
|
|
17840
|
-
}
|
|
17841
|
-
if (!response.ok || !response.body) throw new Error("The requested runtime release is unavailable");
|
|
17842
|
-
const chunks = [];
|
|
17843
|
-
let size = 0;
|
|
17844
|
-
for await (const chunk of response.body) {
|
|
17845
|
-
size += chunk.byteLength;
|
|
17846
|
-
if (size > maxArchiveBytes) throw new Error("Runtime archive exceeds its size limit");
|
|
17847
|
-
chunks.push(chunk);
|
|
17848
|
-
}
|
|
17849
|
-
return Buffer.concat(chunks);
|
|
17850
|
-
}
|
|
17851
|
-
throw new Error("Too many runtime download redirects");
|
|
17852
|
-
}
|
|
17853
|
-
function localPrerequisites() {
|
|
17854
|
-
const runtime = installedLocalRuntime();
|
|
17855
|
-
const directory2 = process.env.BACKIFY_LOCAL_POSTGRES_BIN ?? (process.platform === "win32" ? join(process.env.ProgramFiles ?? "C:/Program Files", "PostgreSQL", "17", "bin") : void 0);
|
|
17856
|
-
const checks = ["initdb", "pg_ctl", "psql"].map((name2) => {
|
|
17857
|
-
const executable = directory2 ? join(directory2, name2 + (process.platform === "win32" ? ".exe" : "")) : name2;
|
|
17858
|
-
const result = spawnSync(executable, ["--version"], { windowsHide: true, timeout: 1e4, encoding: "utf8" });
|
|
17859
|
-
return { name: name2, available: result.status === 0 };
|
|
17860
|
-
});
|
|
17861
|
-
return { rid: runtimeIdentifier(), runtime: runtime ? { installed: true, version: runtime.version } : { installed: false }, postgres: checks, ready: !!runtime && checks.every((check) => check.available) };
|
|
17862
|
-
}
|
|
17728
|
+
// src/browser-auth.ts
|
|
17729
|
+
import { randomBytes } from "node:crypto";
|
|
17730
|
+
import { createServer } from "node:http";
|
|
17863
17731
|
|
|
17864
|
-
// src/
|
|
17865
|
-
import {
|
|
17866
|
-
import {
|
|
17867
|
-
import {
|
|
17868
|
-
import {
|
|
17732
|
+
// src/development.ts
|
|
17733
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync4, renameSync, rmSync, mkdtempSync, lstatSync as lstatSync2, cpSync } from "node:fs";
|
|
17734
|
+
import { dirname as dirname4, join as join3, resolve as resolve3 } from "node:path";
|
|
17735
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
17736
|
+
import { randomUUID } from "node:crypto";
|
|
17869
17737
|
|
|
17870
|
-
// src/
|
|
17871
|
-
import {
|
|
17872
|
-
import {
|
|
17873
|
-
import {
|
|
17874
|
-
import { existsSync as existsSync8, lstatSync as lstatSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync15, realpathSync as realpathSync5, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4 } from "node:fs";
|
|
17875
|
-
import { resolve as resolve15, join as join9, relative as relative7, isAbsolute as isAbsolute12, dirname as dirname9 } from "node:path";
|
|
17738
|
+
// src/setup.ts
|
|
17739
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readdirSync, lstatSync } from "node:fs";
|
|
17740
|
+
import { basename, dirname as dirname3, join as join2, resolve as resolve2, isAbsolute } from "node:path";
|
|
17741
|
+
import { spawnSync } from "node:child_process";
|
|
17876
17742
|
|
|
17877
17743
|
// ../platform/dist/manifest.js
|
|
17878
17744
|
var import_yaml = __toESM(require_dist(), 1);
|
|
@@ -17905,6 +17771,8 @@ function parseApplicationManifest(source) {
|
|
|
17905
17771
|
if (!object(settings))
|
|
17906
17772
|
throw new Error("Capability configuration must be a mapping: " + name2);
|
|
17907
17773
|
validatePortableSettings(settings);
|
|
17774
|
+
if (name2 === "auth")
|
|
17775
|
+
validateAuthSettings(settings);
|
|
17908
17776
|
if (settings.enabled !== void 0 && typeof settings.enabled !== "boolean")
|
|
17909
17777
|
throw new Error("enabled must be boolean: " + name2);
|
|
17910
17778
|
if (name2 === "database" && settings.engine !== void 0 && settings.engine !== "postgres")
|
|
@@ -17934,8 +17802,32 @@ function validatePortableSettings(settings) {
|
|
|
17934
17802
|
}
|
|
17935
17803
|
}
|
|
17936
17804
|
}
|
|
17937
|
-
function
|
|
17938
|
-
|
|
17805
|
+
function validateAuthSettings(settings) {
|
|
17806
|
+
const supported = ["entra", "google", "email"];
|
|
17807
|
+
if (Object.keys(settings).some((key) => !["enabled", "providers", "defaultProvider", "session", "roles"].includes(key)))
|
|
17808
|
+
throw new Error("Unknown authentication setting");
|
|
17809
|
+
if (settings.providers !== void 0 && (!Array.isArray(settings.providers) || settings.providers.length === 0 || settings.providers.some((provider) => typeof provider !== "string" || !supported.includes(provider)) || new Set(settings.providers).size !== settings.providers.length))
|
|
17810
|
+
throw new Error("Authentication requires distinct supported providers: entra, google, email");
|
|
17811
|
+
if (settings.defaultProvider !== void 0 && (!Array.isArray(settings.providers) || !settings.providers.includes(settings.defaultProvider)))
|
|
17812
|
+
throw new Error("The default authentication provider must be declared");
|
|
17813
|
+
if (settings.session !== void 0 && (!object(settings.session) || Object.keys(settings.session).some((key) => !["duration", "refresh"].includes(key)) || typeof settings.session.duration !== "string" || settings.session.duration.trim() !== settings.session.duration || !/^(?:[1-9]|1[0-9]|2[0-4])h$/.test(settings.session.duration) || settings.session.refresh !== void 0 && typeof settings.session.refresh !== "boolean"))
|
|
17814
|
+
throw new Error("Authentication sessions require a 1h-24h duration and an optional boolean refresh setting");
|
|
17815
|
+
if (settings.roles !== void 0 && (!Array.isArray(settings.roles) || settings.roles.length > 32 || settings.roles.some((role) => typeof role !== "string" || !role.trim() || role.length > 100) || new Set(settings.roles).size !== settings.roles.length))
|
|
17816
|
+
throw new Error("Authentication roles must be distinct bounded names");
|
|
17817
|
+
}
|
|
17818
|
+
function manifestDiagnostics(manifest, used, authentication) {
|
|
17819
|
+
const diagnostics = [...new Set(used)].filter((name2) => !manifest.capabilities[name2] || manifest.capabilities[name2]?.enabled === false).map((name2) => ({ code: "undeclared-capability", severity: "error", message: "Declare capabilities." + name2 + " in backify.yaml before deployment" }));
|
|
17820
|
+
if (authentication) {
|
|
17821
|
+
const declared = manifest.capabilities.auth?.providers;
|
|
17822
|
+
if (authentication.dynamicProviders === true && (!Array.isArray(declared) || declared.length === 0))
|
|
17823
|
+
diagnostics.push({ code: "undeclared-auth-providers", severity: "error", message: "Dynamic sign-in requires an explicit capabilities.auth.providers list" });
|
|
17824
|
+
if (Array.isArray(authentication.providers)) {
|
|
17825
|
+
for (const provider of authentication.providers)
|
|
17826
|
+
if (!Array.isArray(declared) || !declared.includes(provider))
|
|
17827
|
+
diagnostics.push({ code: "undeclared-auth-provider", severity: "error", message: "Declare authentication provider " + String(provider) + " before deployment" });
|
|
17828
|
+
}
|
|
17829
|
+
}
|
|
17830
|
+
return diagnostics;
|
|
17939
17831
|
}
|
|
17940
17832
|
|
|
17941
17833
|
// ../platform/dist/delivery.js
|
|
@@ -17968,6 +17860,10 @@ Run the following to discover the connected hosting target, repository, branch,
|
|
|
17968
17860
|
|
|
17969
17861
|
Connection and initialization may proceed without application requirements. Then ask what to build if none were supplied; never invent requirements or record this setup text as a user requirement.
|
|
17970
17862
|
|
|
17863
|
+
## Reauthenticate CLI access
|
|
17864
|
+
|
|
17865
|
+
If CLI authentication expires or is rejected, ask the user to reauthenticate. Run npx @backifydev/cli@latest connect --reauth from the application folder (or supply --app <app-id> and --organization <organization-id>). It opens https://auth.backify.dev for sign-in and explicit approval of 7, 14, 30, or 90 days of application CLI access. Keep the command running while the user signs in; it saves the new credential automatically and the browser can then be closed. Never ask the user to paste tokens, inspect credential files, or copy a new setup prompt just to reconnect. Ordinary authenticated commands also open browser recovery on expiry or HTTP 401 and retry once after approval. For unattended execution use --no-browser; with --json an authentication_required error includes a reauthenticationCommand. After explicit reconnection, retry only an operation known to have failed authentication; do not repeat a completed deployment or other successful write.
|
|
17866
|
+
|
|
17971
17867
|
## Record each user request
|
|
17972
17868
|
|
|
17973
17869
|
For EVERY user request to build or modify this application, including the initial requirements supplied alongside or after setup, record only the user's own requirement text before changing application code. Submit the text through standard input to:
|
|
@@ -18049,7 +17945,7 @@ var MAIL_LIMITS = Object.freeze({
|
|
|
18049
17945
|
});
|
|
18050
17946
|
var capability = (name2, imp, description, stateful = false, provisioner = name2) => ({ name: name2, import: imp, version: 1, stateful, provisioner, docs: `${DOCS_URL}/capabilities/${name2}`, description, destructiveRemovalRequiresApproval: stateful });
|
|
18051
17947
|
var CAPABILITIES = [
|
|
18052
|
-
capability("auth", "auth", "Application users, organizations, roles, and permissions"),
|
|
17948
|
+
capability("auth", "auth", "Application users, organizations, roles, and permissions", true),
|
|
18053
17949
|
capability("database", "db", "Persistent application data", true),
|
|
18054
17950
|
capability("storage", "storage", "Persistent files and objects", true),
|
|
18055
17951
|
capability("functions", "functions", "Server function declarations"),
|
|
@@ -18163,113 +18059,931 @@ function validateConfig(value) {
|
|
|
18163
18059
|
}
|
|
18164
18060
|
var BACKIFY_SCHEMA = { $schema: "https://json-schema.org/draft/2020-12/schema", $id: `${DOCS_URL}/schema/backify.json`, type: "object", additionalProperties: false, required: ["$schema", "platform", "sdk", "appId", "instructionVersion", "docs", "instructions"], properties: { $schema: { const: `${DOCS_URL}/schema/backify.json` }, platform: { const: "backify" }, sdk: { const: SDK_PACKAGE }, appId: { type: "string", pattern: "^app_[A-Za-z0-9_-]+$" }, instructionVersion: { type: "integer", minimum: 1 }, agent: { enum: ["codex", "claude", "copilot", "gemini", "custom"] }, instructionFile: { type: "string", minLength: 1 }, docs: { type: "string", format: "uri" }, instructions: { type: "object", additionalProperties: false, required: ["preferBackifyCapabilities", "allowDirectCloudInfrastructure"], properties: { preferBackifyCapabilities: { const: true }, allowDirectCloudInfrastructure: { const: false } } } } };
|
|
18165
18061
|
|
|
18166
|
-
//
|
|
18167
|
-
import
|
|
18168
|
-
import {
|
|
18169
|
-
import {
|
|
18170
|
-
var
|
|
18171
|
-
function
|
|
18172
|
-
const
|
|
18173
|
-
|
|
18174
|
-
|
|
18175
|
-
|
|
18176
|
-
|
|
18177
|
-
|
|
18062
|
+
// src/delivery.ts
|
|
18063
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
18064
|
+
import { dirname as dirname2, join, relative as relativePath } from "node:path";
|
|
18065
|
+
import { createHash } from "node:crypto";
|
|
18066
|
+
var previousGeneratedWorkflowHashes = /* @__PURE__ */ new Set(["33e962274e8493f7cbac2afbac05a4e96afeec49ee08cf2bf82f8add9081b28b", "578dfb4c771c623fd04454e8a604efd701d5563d11e11191324862369bd24718", "6eb280ae7bdb14c390011686c41400f59c023304c3015d8440b7e80aaafa1c11", "545e03a1d5b996ade179e0e60ed5c86accdadc1982c1c25efd094e34f72f47ee"]);
|
|
18067
|
+
function validateDeliveryInstructions(root) {
|
|
18068
|
+
for (const filename2 of ["BACKIFY.md", "BACKIFY-WORKFLOW.md"]) {
|
|
18069
|
+
const path = join(root, filename2);
|
|
18070
|
+
if (existsSync2(path)) {
|
|
18071
|
+
const existing = readFileSync(path, "utf8").replace(/\r\n/g, "\n");
|
|
18072
|
+
if (existing !== deliveryInstructions && !previousGeneratedWorkflowHashes.has(createHash("sha256").update(existing).digest("hex"))) throw new Error(filename2 + " contains different instructions. Review it before initializing; it will not be overwritten.");
|
|
18073
|
+
}
|
|
18074
|
+
}
|
|
18178
18075
|
}
|
|
18179
|
-
function
|
|
18180
|
-
|
|
18181
|
-
|
|
18182
|
-
|
|
18183
|
-
|
|
18184
|
-
const
|
|
18185
|
-
|
|
18186
|
-
|
|
18187
|
-
|
|
18076
|
+
function installDeliveryInstructions(root, instructionFiles2) {
|
|
18077
|
+
validateDeliveryInstructions(root);
|
|
18078
|
+
writeFileSync2(join(root, "BACKIFY.md"), deliveryInstructions);
|
|
18079
|
+
for (const relative11 of instructionFiles2) {
|
|
18080
|
+
const instructionPath = join(root, relative11), content = readFileSync(instructionPath, "utf8");
|
|
18081
|
+
const link = relativePath(dirname2(instructionPath), join(root, "BACKIFY.md")).replaceAll("\\", "/");
|
|
18082
|
+
const legacyLink = relativePath(dirname2(instructionPath), join(root, "BACKIFY-WORKFLOW.md")).replaceAll("\\", "/");
|
|
18083
|
+
const legacy = "For application delivery and recording each user request, follow [BACKIFY-WORKFLOW.md](" + legacyLink + ").";
|
|
18084
|
+
const guidance = "For application delivery and recording each user request, follow [BACKIFY.md](" + link + ").";
|
|
18085
|
+
if (content.includes(legacy)) writeFileSync2(instructionPath, content.replace(legacy, guidance));
|
|
18086
|
+
else if (!content.includes("## Backify delivery")) writeFileSync2(instructionPath, content.trimEnd() + "\n\n## Backify delivery\n\n" + guidance + "\n");
|
|
18087
|
+
}
|
|
18088
|
+
}
|
|
18089
|
+
|
|
18090
|
+
// src/setup.ts
|
|
18091
|
+
var instructionFiles = ["AGENTS.md"];
|
|
18092
|
+
var agentInstructionFiles = { codex: "AGENTS.md", claude: "CLAUDE.md", copilot: join2(".github", "copilot-instructions.md"), gemini: "GEMINI.md" };
|
|
18093
|
+
function instructionSelection(root, agent, instructionFile) {
|
|
18094
|
+
const configPath2 = join2(root, "backify.json"), configured = existsSync3(configPath2) ? JSON.parse(readFileSync2(configPath2, "utf8")) : {};
|
|
18095
|
+
const custom = instructionFile ?? (agent === void 0 ? configured.instructionFile : void 0);
|
|
18096
|
+
const selected = instructionFile !== void 0 ? "custom" : agent ?? configured.agent ?? "codex";
|
|
18097
|
+
if (typeof selected !== "string" || selected !== "custom" && !Object.hasOwn(agentInstructionFiles, selected)) throw new Error("Select the current tool with --agent or --instruction-file <relative-path>.");
|
|
18098
|
+
const filename2 = custom ?? agentInstructionFiles[selected];
|
|
18099
|
+
if (typeof filename2 !== "string" || !filename2.trim()) throw new Error("Select the current tool with --agent codex, claude, copilot, or gemini, or --instruction-file <relative-path> for any other tool.");
|
|
18100
|
+
const normalized = filename2.replaceAll("\\", "/"), parts = normalized.split("/");
|
|
18101
|
+
if (isAbsolute(filename2) || /^[A-Za-z]:/.test(filename2) || parts.some((part) => !part || part === "." || part === ".." || part.toLowerCase() === ".git") || ["backify.md", "backify-workflow.md", "backify.json", "package.json", "package-lock.json"].includes(normalized.toLowerCase())) throw new Error("Instruction file must be a relative tool-instruction path inside the project, not Git metadata or a Backify configuration file.");
|
|
18102
|
+
for (let index = 1; index <= parts.length; index++) {
|
|
18103
|
+
const entry = join2(root, ...parts.slice(0, index));
|
|
18104
|
+
try {
|
|
18105
|
+
const metadata = lstatSync(entry);
|
|
18106
|
+
if (metadata.isSymbolicLink() || index < parts.length && !metadata.isDirectory() || index === parts.length && !metadata.isFile()) throw new Error("Instruction file path cannot traverse symlinks or replace a directory.");
|
|
18107
|
+
} catch (reason) {
|
|
18108
|
+
if (reason.code !== "ENOENT") throw reason;
|
|
18188
18109
|
}
|
|
18189
|
-
|
|
18190
|
-
|
|
18191
|
-
|
|
18192
|
-
|
|
18193
|
-
|
|
18194
|
-
|
|
18195
|
-
|
|
18196
|
-
|
|
18197
|
-
|
|
18198
|
-
|
|
18199
|
-
|
|
18200
|
-
|
|
18201
|
-
|
|
18202
|
-
|
|
18203
|
-
|
|
18204
|
-
|
|
18205
|
-
|
|
18206
|
-
|
|
18207
|
-
}
|
|
18208
|
-
}
|
|
18110
|
+
}
|
|
18111
|
+
return { agent: selected, instructionFile: normalized, files: [normalized] };
|
|
18112
|
+
}
|
|
18113
|
+
function selectedInstructionFiles(root = process.cwd(), agent, instructionFile) {
|
|
18114
|
+
return instructionSelection(root, agent, instructionFile).files;
|
|
18115
|
+
}
|
|
18116
|
+
function isToolWorkspaceEntry(entry, root = process.cwd()) {
|
|
18117
|
+
if (entry.name.toLowerCase() === ".git" || entry.name.toLowerCase() === ".env" || entry.name.toLowerCase().startsWith(".env.")) return false;
|
|
18118
|
+
return entry.isDirectory() && (entry.name.startsWith(".") || ["work", "outputs"].includes(entry.name) || readdirSync(join2(root, entry.name)).length === 0) || entry.isFile() && (entry.name.startsWith(".") || /\.(md|mdc)$/i.test(entry.name));
|
|
18119
|
+
}
|
|
18120
|
+
function detectPackageManager(root = process.cwd()) {
|
|
18121
|
+
for (const [file, name2] of [["pnpm-lock.yaml", "pnpm"], ["yarn.lock", "yarn"], ["bun.lockb", "bun"], ["bun.lock", "bun"], ["package-lock.json", "npm"]]) if (existsSync3(join2(root, file))) return name2;
|
|
18122
|
+
return "npm";
|
|
18123
|
+
}
|
|
18124
|
+
function updateInstructions(root = process.cwd(), agent, instructionFile) {
|
|
18125
|
+
const updates = selectedInstructionFiles(root, agent, instructionFile).map((relative11) => {
|
|
18126
|
+
const path = join2(root, relative11);
|
|
18127
|
+
return { path, content: updateManagedInstructions(existsSync3(path) ? readFileSync2(path, "utf8") : "") };
|
|
18209
18128
|
});
|
|
18210
|
-
const
|
|
18211
|
-
|
|
18212
|
-
|
|
18213
|
-
|
|
18214
|
-
used.add("scheduler");
|
|
18215
|
-
if (ts.isIdentifier(node)) {
|
|
18216
|
-
const parent = node.parent;
|
|
18217
|
-
if (named.has(node.text) && !ts.isImportSpecifier(parent) && isValueUse(node))
|
|
18218
|
-
used.add(named.get(node.text));
|
|
18219
|
-
}
|
|
18220
|
-
if (ts.isPropertyAccessExpression(node)) {
|
|
18221
|
-
if (ts.isIdentifier(node.expression)) {
|
|
18222
|
-
if (namespaces.has(node.expression.text)) {
|
|
18223
|
-
const cap = SDK_IMPORT_CAPABILITIES[node.name.text];
|
|
18224
|
-
if (cap)
|
|
18225
|
-
used.add(cap);
|
|
18226
|
-
}
|
|
18227
|
-
if (roots.has(node.expression.text)) {
|
|
18228
|
-
const cap = SDK_IMPORT_CAPABILITIES[node.name.text];
|
|
18229
|
-
if (cap)
|
|
18230
|
-
used.add(cap);
|
|
18231
|
-
}
|
|
18232
|
-
}
|
|
18233
|
-
if (node.name.text === "schedule" && rootCapability(node.expression, named, namespaces, roots) === "jobs")
|
|
18234
|
-
used.add("scheduler");
|
|
18235
|
-
}
|
|
18236
|
-
ts.forEachChild(node, visit);
|
|
18237
|
-
};
|
|
18238
|
-
visit(source);
|
|
18129
|
+
for (const { path, content } of updates) {
|
|
18130
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
18131
|
+
writeFileSync3(path, content);
|
|
18132
|
+
}
|
|
18239
18133
|
}
|
|
18240
|
-
function
|
|
18241
|
-
const
|
|
18242
|
-
|
|
18134
|
+
function synchronize(root = process.cwd(), agent, instructionFile) {
|
|
18135
|
+
const path = join2(root, "backify.json");
|
|
18136
|
+
if (!existsSync3(path)) throw new Error("backify.json is missing; run backify init --app app_xxxxx");
|
|
18137
|
+
const value = JSON.parse(readFileSync2(path, "utf8")), issues = validateConfig(value);
|
|
18138
|
+
if (issues.length) throw new Error(issues.join("; "));
|
|
18139
|
+
if (value.instructionVersion > INSTRUCTION_VERSION) throw new Error("This app uses newer instructions. Install a matching or newer CLI before syncing.");
|
|
18140
|
+
const selected = instructionSelection(root, agent, instructionFile);
|
|
18141
|
+
validateDeliveryInstructions(root);
|
|
18142
|
+
updateInstructions(root, selected.agent, selected.instructionFile);
|
|
18143
|
+
installDeliveryInstructions(root, selected.files);
|
|
18144
|
+
value.agent = selected.agent;
|
|
18145
|
+
value.instructionFile = selected.instructionFile;
|
|
18146
|
+
value.instructionVersion = INSTRUCTION_VERSION;
|
|
18147
|
+
writeFileSync3(path, JSON.stringify(value, null, 2) + "\n");
|
|
18243
18148
|
}
|
|
18244
|
-
function
|
|
18245
|
-
|
|
18246
|
-
|
|
18247
|
-
|
|
18248
|
-
|
|
18249
|
-
|
|
18149
|
+
function sdkInstalled(root = process.cwd()) {
|
|
18150
|
+
try {
|
|
18151
|
+
const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
|
|
18152
|
+
return Boolean(pkg.dependencies?.[SDK_PACKAGE] || pkg.devDependencies?.[SDK_PACKAGE]);
|
|
18153
|
+
} catch {
|
|
18154
|
+
return false;
|
|
18250
18155
|
}
|
|
18251
|
-
return void 0;
|
|
18252
18156
|
}
|
|
18253
|
-
function
|
|
18254
|
-
|
|
18255
|
-
const
|
|
18256
|
-
|
|
18257
|
-
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".backify" || entry.name.startsWith(".git"))
|
|
18258
|
-
continue;
|
|
18259
|
-
const path = join2(dir, entry.name);
|
|
18260
|
-
if (entry.isDirectory())
|
|
18261
|
-
walk(path);
|
|
18262
|
-
else if ([".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs"].includes(extname(path)))
|
|
18263
|
-
result.push({ path, content: readFileSync2(path, "utf8") });
|
|
18264
|
-
}
|
|
18265
|
-
};
|
|
18266
|
-
if (existsSync3(root))
|
|
18267
|
-
walk(root);
|
|
18268
|
-
return result;
|
|
18157
|
+
function installSdk(root = process.cwd()) {
|
|
18158
|
+
if (sdkInstalled(root)) return;
|
|
18159
|
+
const manager = detectPackageManager(root), args2 = manager === "npm" ? ["install", SDK_PACKAGE] : ["add", SDK_PACKAGE], result = spawnSync(manager, args2, { cwd: root, stdio: "inherit", shell: process.platform === "win32" });
|
|
18160
|
+
if (result.status !== 0) throw new Error(`Could not install ${SDK_PACKAGE} with ${manager}`);
|
|
18269
18161
|
}
|
|
18270
|
-
|
|
18271
|
-
|
|
18272
|
-
|
|
18162
|
+
function ensurePackageManifest(root) {
|
|
18163
|
+
const path = join2(root, "package.json");
|
|
18164
|
+
if (existsSync3(path)) return;
|
|
18165
|
+
const setupFiles = /* @__PURE__ */ new Set([".git", ".gitignore", ".github", "AGENTS.md", "CLAUDE.md", "GEMINI.md", "BACKIFY.md", "BACKIFY-WORKFLOW.md", "README.md", "LICENSE", "backify.json"]);
|
|
18166
|
+
if (readdirSync(root, { withFileTypes: true }).some((entry) => !setupFiles.has(entry.name) && !isToolWorkspaceEntry(entry, root))) throw new Error("package.json is missing. Run backify init from the existing application's JavaScript/TypeScript package directory, or start in an empty folder. See https://agent.backify.dev/");
|
|
18167
|
+
const name2 = basename(resolve2(root)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "backify-app";
|
|
18168
|
+
writeFileSync3(path, JSON.stringify({ name: name2, version: "0.0.0", private: true, type: "module" }, null, 2) + "\n", { flag: "wx" });
|
|
18169
|
+
}
|
|
18170
|
+
function initialize(appId, root = process.cwd(), install = true, agent, instructionFile) {
|
|
18171
|
+
if (!/^app_[A-Za-z0-9_-]+$/.test(appId)) throw new Error("Provide an application ID using --app app_xxxxx");
|
|
18172
|
+
const selected = instructionSelection(root, agent, instructionFile), files = selected.files;
|
|
18173
|
+
validateDeliveryInstructions(root);
|
|
18174
|
+
for (const relative11 of files) if (existsSync3(join2(root, relative11))) updateManagedInstructions(readFileSync2(join2(root, relative11), "utf8"));
|
|
18175
|
+
ensurePackageManifest(root);
|
|
18176
|
+
if (install) installSdk(root);
|
|
18177
|
+
const configPath2 = join2(root, "backify.json"), existing = existsSync3(configPath2) ? JSON.parse(readFileSync2(configPath2, "utf8")) : {};
|
|
18178
|
+
writeFileSync3(configPath2, JSON.stringify({ ...existing, ...createConfig(appId), agent: selected.agent, instructionFile: selected.instructionFile }, null, 2) + "\n");
|
|
18179
|
+
updateInstructions(root, selected.agent, selected.instructionFile);
|
|
18180
|
+
installDeliveryInstructions(root, files);
|
|
18181
|
+
}
|
|
18182
|
+
function doctor(root = process.cwd(), agent, instructionFile) {
|
|
18183
|
+
const issues = [], warnings = [];
|
|
18184
|
+
if (!sdkInstalled(root)) issues.push(`${SDK_PACKAGE} is not installed`);
|
|
18185
|
+
let config;
|
|
18186
|
+
try {
|
|
18187
|
+
config = JSON.parse(readFileSync2(join2(root, "backify.json"), "utf8"));
|
|
18188
|
+
issues.push(...validateConfig(config));
|
|
18189
|
+
} catch {
|
|
18190
|
+
issues.push("backify.json is missing or invalid JSON");
|
|
18191
|
+
}
|
|
18192
|
+
if (config && config.instructionVersion !== INSTRUCTION_VERSION) issues.push(`Repository instruction version ${config.instructionVersion}; CLI version ${INSTRUCTION_VERSION}. Use the matching CLI or run backify sync to upgrade.`);
|
|
18193
|
+
let files = instructionFiles;
|
|
18194
|
+
try {
|
|
18195
|
+
files = selectedInstructionFiles(root, agent, instructionFile);
|
|
18196
|
+
} catch (reason) {
|
|
18197
|
+
issues.push(reason instanceof Error ? reason.message : String(reason));
|
|
18198
|
+
}
|
|
18199
|
+
for (const relative11 of files) {
|
|
18200
|
+
const path = join2(root, relative11);
|
|
18201
|
+
if (!existsSync3(path)) {
|
|
18202
|
+
issues.push(`${relative11} is missing`);
|
|
18203
|
+
continue;
|
|
18204
|
+
}
|
|
18205
|
+
issues.push(...managedInstructionIssues(readFileSync2(path, "utf8")).map((issue) => `${relative11} ${issue}`));
|
|
18206
|
+
}
|
|
18207
|
+
let version = "unknown";
|
|
18208
|
+
try {
|
|
18209
|
+
const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
|
|
18210
|
+
version = pkg.dependencies?.[SDK_PACKAGE] ?? pkg.devDependencies?.[SDK_PACKAGE] ?? "missing";
|
|
18211
|
+
} catch {
|
|
18212
|
+
}
|
|
18213
|
+
return { valid: issues.length === 0, appId: config?.appId, sdkVersion: version, instructionVersion: config?.instructionVersion, currentInstructionVersion: INSTRUCTION_VERSION, issues, warnings };
|
|
18214
|
+
}
|
|
18215
|
+
|
|
18216
|
+
// src/development.ts
|
|
18217
|
+
function promptMetadata(args2) {
|
|
18218
|
+
const fields5 = [["--model", "model", 200], ["--ai-tool", "aiTool", 200], ["--access-level", "accessLevel", 500]];
|
|
18219
|
+
const metadata = {};
|
|
18220
|
+
for (const [flag, field, maxLength] of fields5) {
|
|
18221
|
+
const index = args2.indexOf(flag);
|
|
18222
|
+
if (index < 0) continue;
|
|
18223
|
+
const value = args2[index + 1]?.trim();
|
|
18224
|
+
if (!value || value.startsWith("--") || value.length > maxLength || /[\u0000-\u001f\u007f-\u009f]|bfcli_[0-9a-f]{64}|--pair\s+[0-9a-f]{64}/i.test(value))
|
|
18225
|
+
throw new Error(`Provide ${flag} with a nonempty value of at most ${maxLength} characters without control characters or credentials.`);
|
|
18226
|
+
metadata[field] = value;
|
|
18227
|
+
}
|
|
18228
|
+
return metadata;
|
|
18229
|
+
}
|
|
18230
|
+
function applicationGuid(appId) {
|
|
18231
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(appId)) return appId.toLowerCase();
|
|
18232
|
+
const match = /^app_([0-9a-f]{32})$/i.exec(appId);
|
|
18233
|
+
if (!match) throw new Error("Use the application ID supplied by Backify.");
|
|
18234
|
+
const value = match[1];
|
|
18235
|
+
return [value.slice(0, 8), value.slice(8, 12), value.slice(12, 16), value.slice(16, 20), value.slice(20)].join("-");
|
|
18236
|
+
}
|
|
18237
|
+
function projectApplication(root = process.cwd()) {
|
|
18238
|
+
if (!existsSync4(join3(root, "backify.json"))) throw new Error("Run backify connect --app <application-id>, then backify init.");
|
|
18239
|
+
return JSON.parse(readFileSync3(join3(root, "backify.json"), "utf8")).appId;
|
|
18240
|
+
}
|
|
18241
|
+
function apiAddress(value) {
|
|
18242
|
+
const address = new URL(value);
|
|
18243
|
+
if (address.username || address.password || address.search || address.hash || address.protocol !== "https:" && !(address.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(address.hostname))) throw new Error("Backify API must use HTTPS (HTTP is allowed only on loopback for local development).");
|
|
18244
|
+
return address.toString().replace(/\/$/, "");
|
|
18245
|
+
}
|
|
18246
|
+
var CliAuthenticationRequired = class extends Error {
|
|
18247
|
+
code = "authentication_required";
|
|
18248
|
+
constructor() {
|
|
18249
|
+
super("Backify CLI authentication expired or was revoked. Ask the user to sign in, then run backify connect --reauth. The browser lets them choose 7, 14, 30, or 90 days.");
|
|
18250
|
+
}
|
|
18251
|
+
};
|
|
18252
|
+
function apiClient(session, transport = fetch) {
|
|
18253
|
+
const base = apiAddress(session.apiUrl);
|
|
18254
|
+
return async (path, init) => {
|
|
18255
|
+
if (!path.startsWith("/") || path.startsWith("//")) throw new Error("Invalid API path.");
|
|
18256
|
+
if (session.expiresAt && Date.parse(session.expiresAt) <= Date.now()) throw new CliAuthenticationRequired();
|
|
18257
|
+
const response = await transport(base + path, { ...init, redirect: "error", signal: AbortSignal.timeout(3e4), headers: { "content-type": "application/json", "user-agent": cliUserAgent, "X-Organization-ID": session.organizationId, ...session.token ? { authorization: "Bearer " + session.token } : {}, ...init?.headers } });
|
|
18258
|
+
if (!response.ok) {
|
|
18259
|
+
if (response.status === 401) throw new CliAuthenticationRequired();
|
|
18260
|
+
let message = "Backify request failed (" + response.status + ").";
|
|
18261
|
+
try {
|
|
18262
|
+
const problem = await response.json();
|
|
18263
|
+
message = [problem.code, problem.title].filter(Boolean).join(": ") || message;
|
|
18264
|
+
} catch {
|
|
18265
|
+
}
|
|
18266
|
+
throw new Error(message);
|
|
18267
|
+
}
|
|
18268
|
+
return response.status === 204 ? void 0 : await response.json();
|
|
18269
|
+
};
|
|
18270
|
+
}
|
|
18271
|
+
function saveUserConfig(path, config) {
|
|
18272
|
+
const temporary = path + "." + randomUUID() + ".tmp";
|
|
18273
|
+
try {
|
|
18274
|
+
writeFileSync4(temporary, JSON.stringify(config, null, 2) + "\n", { mode: 384, flag: "wx" });
|
|
18275
|
+
if (process.platform === "win32") {
|
|
18276
|
+
const account = [process.env.USERDOMAIN, process.env.USERNAME].filter(Boolean).join("\\");
|
|
18277
|
+
if (!account) throw new Error("Cannot identify the account to protect CLI credentials.");
|
|
18278
|
+
const secured = spawnSync2("icacls", [temporary, "/inheritance:r", "/grant:r", account + ":(F)"], { windowsHide: true, encoding: "utf8" });
|
|
18279
|
+
if (secured.status !== 0) throw new Error("Could not restrict CLI credential file permissions.");
|
|
18280
|
+
}
|
|
18281
|
+
renameSync(temporary, path);
|
|
18282
|
+
} finally {
|
|
18283
|
+
rmSync(temporary, { force: true });
|
|
18284
|
+
}
|
|
18285
|
+
}
|
|
18286
|
+
function repositoryName(remote) {
|
|
18287
|
+
const match = /^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?\/?$/i.exec(remote);
|
|
18288
|
+
if (!match) throw new Error("Repository must be a credential-free GitHub URL.");
|
|
18289
|
+
return match[1].toLowerCase();
|
|
18290
|
+
}
|
|
18291
|
+
function git(args2, root = process.cwd(), environment = process.env) {
|
|
18292
|
+
const result = spawnSync2("git", args2, { cwd: root, env: environment, encoding: "utf8", windowsHide: true, maxBuffer: 2 * 1024 * 1024 });
|
|
18293
|
+
if (result.status !== 0) throw new Error("Git " + args2[0] + " failed. Check repository access, branch protection, and local changes.");
|
|
18294
|
+
return result.stdout.trim();
|
|
18295
|
+
}
|
|
18296
|
+
function gitEnvironment(repository, token) {
|
|
18297
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) throw new Error("Invalid repository.");
|
|
18298
|
+
return { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_COUNT: "3", GIT_CONFIG_KEY_0: "http.https://github.com/" + repository + ".git.extraheader", GIT_CONFIG_VALUE_0: "Authorization: Basic " + Buffer.from("x-access-token:" + token).toString("base64"), GIT_CONFIG_KEY_1: "credential.helper", GIT_CONFIG_VALUE_1: "", GIT_CONFIG_KEY_2: "http.followRedirects", GIT_CONFIG_VALUE_2: "false" };
|
|
18299
|
+
}
|
|
18300
|
+
function validateCloneMerge(source, target) {
|
|
18301
|
+
for (const entry of readdirSync2(source, { withFileTypes: true })) {
|
|
18302
|
+
const destination = join3(target, entry.name);
|
|
18303
|
+
let existing;
|
|
18304
|
+
try {
|
|
18305
|
+
existing = lstatSync2(destination);
|
|
18306
|
+
} catch (reason) {
|
|
18307
|
+
if (reason.code === "ENOENT") continue;
|
|
18308
|
+
throw reason;
|
|
18309
|
+
}
|
|
18310
|
+
if (entry.isDirectory() && existing.isDirectory() && !existing.isSymbolicLink()) validateCloneMerge(join3(source, entry.name), destination);
|
|
18311
|
+
else throw new Error("Clone conflicts with existing workspace entry: " + destination + ". Existing files are never overwritten; reconcile the conflict or use a new folder.");
|
|
18312
|
+
}
|
|
18313
|
+
}
|
|
18314
|
+
async function cloneProject(call2, appId, root = process.cwd()) {
|
|
18315
|
+
const status = await call2("/apps/" + applicationGuid(appId) + "/developer");
|
|
18316
|
+
if (!status.repository || !status.branch || !status.workflowReady) throw new Error("Connect the repository in Backify and wait for workflow setup.");
|
|
18317
|
+
if (readdirSync2(root, { withFileTypes: true }).some((entry) => !isToolWorkspaceEntry(entry, root))) throw new Error("--clone requires an empty directory or a workspace containing only AI tool metadata and default work/outputs directories. Existing application files and Git repositories are never overwritten.");
|
|
18318
|
+
const credential = await call2("/apps/" + applicationGuid(appId) + "/developer/git-credential", { method: "POST" });
|
|
18319
|
+
if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed. Run connect again.");
|
|
18320
|
+
const staging = mkdtempSync(join3(dirname4(resolve3(root)), ".backify-clone-"));
|
|
18321
|
+
try {
|
|
18322
|
+
git(["clone", "--single-branch", "--branch", status.branch, "--", "https://github.com/" + status.repository + ".git", "."], staging, gitEnvironment(status.repository, credential.token));
|
|
18323
|
+
validateCloneMerge(staging, root);
|
|
18324
|
+
for (const entry of readdirSync2(staging)) cpSync(join3(staging, entry), join3(root, entry), { recursive: true, force: false, errorOnExist: true, dereference: false });
|
|
18325
|
+
} finally {
|
|
18326
|
+
if (dirname4(staging) !== dirname4(resolve3(root)) || !staging.startsWith(join3(dirname4(resolve3(root)), ".backify-clone-"))) throw new Error("Refusing to clean up an unexpected clone staging path.");
|
|
18327
|
+
rmSync(staging, { recursive: true, force: true });
|
|
18328
|
+
}
|
|
18329
|
+
}
|
|
18330
|
+
async function pullProject(call2, appId, root = process.cwd()) {
|
|
18331
|
+
if (existsSync4(join3(root, "backify.json")) && applicationGuid(projectApplication(root)) !== applicationGuid(appId)) throw new Error("Local application ID does not match the connected application.");
|
|
18332
|
+
const prefix = "/apps/" + applicationGuid(appId);
|
|
18333
|
+
const status = await call2(prefix + "/developer");
|
|
18334
|
+
if (!status.repository || !status.branch) throw new Error("Connect the repository and branch in Backify before pulling.");
|
|
18335
|
+
if (resolve3(git(["rev-parse", "--show-toplevel"], root)) !== resolve3(root)) throw new Error("Run pull from the connected repository root.");
|
|
18336
|
+
if (repositoryName(git(["remote", "get-url", "origin"], root)) !== status.repository.toLowerCase()) throw new Error("Local origin does not match the repository connected in Backify.");
|
|
18337
|
+
if (git(["branch", "--show-current"], root) !== status.branch) throw new Error("Local branch does not match the branch connected in Backify. The CLI never switches branches automatically.");
|
|
18338
|
+
git(["check-ref-format", "--branch", status.branch], root);
|
|
18339
|
+
const credential = await call2(prefix + "/developer/git-credential", { method: "POST" });
|
|
18340
|
+
if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed; reconnect before pulling.");
|
|
18341
|
+
git(["fetch", "--no-tags", "--", "https://github.com/" + status.repository + ".git", "refs/heads/" + status.branch + ":refs/remotes/origin/" + status.branch], root, gitEnvironment(status.repository, credential.token));
|
|
18342
|
+
const remoteCommit = git(["rev-parse", "FETCH_HEAD"], root);
|
|
18343
|
+
const counts = git(["rev-list", "--left-right", "--count", "HEAD..." + remoteCommit], root).split(/\s+/).map(Number);
|
|
18344
|
+
if (counts.length !== 2 || counts.some((count) => !Number.isSafeInteger(count) || count < 0)) throw new Error("Could not compare local and remote history.");
|
|
18345
|
+
const [ahead, behind] = counts;
|
|
18346
|
+
if (!behind) return { status: "up-to-date", ahead, behind, commit: git(["rev-parse", "HEAD"], root) };
|
|
18347
|
+
if (ahead) return { status: "blocked", reason: "Local and GitHub history have diverged. Review and reconcile them explicitly; no rebase, reset, or force-push was performed.", ahead, behind };
|
|
18348
|
+
if (git(["status", "--porcelain"], root)) return { status: "blocked", reason: "Local changes or untracked files are present. Commit, ignore, or otherwise resolve them explicitly before updating; nothing was stashed or overwritten.", ahead, behind };
|
|
18349
|
+
git(["merge", "--ff-only", "--", remoteCommit], root);
|
|
18350
|
+
return { status: "updated", ahead: 0, behind: 0, commit: git(["rev-parse", "HEAD"], root) };
|
|
18351
|
+
}
|
|
18352
|
+
async function readPrompt(input) {
|
|
18353
|
+
const chunks = [];
|
|
18354
|
+
let size = 0;
|
|
18355
|
+
for await (const chunk of input) {
|
|
18356
|
+
const bytes = Buffer.from(chunk);
|
|
18357
|
+
size += bytes.length;
|
|
18358
|
+
if (size > 128e3) throw new Error("Prompt exceeds the size limit.");
|
|
18359
|
+
chunks.push(bytes);
|
|
18360
|
+
}
|
|
18361
|
+
const text3 = Buffer.concat(chunks).toString("utf8");
|
|
18362
|
+
if (!text3.trim() || text3.length > 32e3) throw new Error("Provide between 1 and 32000 characters of user prompt text.");
|
|
18363
|
+
if (/\bbfcli_[0-9a-f]{64}\b|--pair\s+[0-9a-f]{64}/i.test(text3)) throw new Error("Do not record setup prompts or Backify credentials.");
|
|
18364
|
+
return text3;
|
|
18365
|
+
}
|
|
18366
|
+
function attachmentNames(values) {
|
|
18367
|
+
if (values.length > 50) throw new Error("At most 50 attachment filenames are allowed.");
|
|
18368
|
+
return [...new Set(values.map((value) => {
|
|
18369
|
+
const filename2 = value.replace(/\\/g, "/").split("/").pop();
|
|
18370
|
+
if (!filename2 || filename2 === "." || filename2 === ".." || filename2.length > 255 || /[\x00-\x1f\x7f]/.test(filename2)) throw new Error("Invalid attachment filename.");
|
|
18371
|
+
return filename2;
|
|
18372
|
+
}))];
|
|
18373
|
+
}
|
|
18374
|
+
function localReport(runChecks, root = process.cwd()) {
|
|
18375
|
+
const pkg = JSON.parse(readFileSync3(join3(root, "package.json"), "utf8"));
|
|
18376
|
+
const dependencies = { ...pkg.devDependencies, ...pkg.dependencies };
|
|
18377
|
+
const framework = ["next", "vite", "astro", "@sveltejs/kit", "nuxt", "react", "express"].find((name2) => dependencies[name2]) ?? "unknown";
|
|
18378
|
+
const setup = doctor(root);
|
|
18379
|
+
const checks = [{ name: "backify doctor", status: setup.valid ? "passed" : "failed" }];
|
|
18380
|
+
const manager = detectPackageManager(root);
|
|
18381
|
+
for (const name2 of ["typecheck", "lint", "test", "build"]) {
|
|
18382
|
+
if (!pkg.scripts?.[name2]) continue;
|
|
18383
|
+
const result = runChecks ? spawnSync2(manager, ["run", name2], { cwd: root, stdio: ["ignore", "inherit", "inherit"], env: { ...process.env, CI: "true" }, shell: process.platform === "win32", windowsHide: true, timeout: 10 * 6e4 }) : void 0;
|
|
18384
|
+
checks.push({ name: name2, status: result ? result.status === 0 ? "passed" : "failed" : "not-run" });
|
|
18385
|
+
}
|
|
18386
|
+
let branch = null, commitSha = null, dirty = true;
|
|
18387
|
+
try {
|
|
18388
|
+
branch = git(["branch", "--show-current"], root);
|
|
18389
|
+
commitSha = git(["rev-parse", "HEAD"], root);
|
|
18390
|
+
dirty = Boolean(git(["status", "--porcelain"], root));
|
|
18391
|
+
} catch {
|
|
18392
|
+
}
|
|
18393
|
+
const outputDirectory = ["dist", "build", "out", "."].find((directory2) => existsSync4(join3(root, directory2, "index.html"))) ?? null;
|
|
18394
|
+
return { framework, sdkVersion: dependencies["@backifydev/sdk"] ?? null, branch, commitSha, dirty, buildCommand: pkg.scripts?.build ? manager + " run build" : null, outputDirectory, hasDockerfile: existsSync4(join3(root, "Dockerfile")), checks };
|
|
18395
|
+
}
|
|
18396
|
+
function matchesSource(release2, commit) {
|
|
18397
|
+
return release2?.commitSha === commit || release2?.sourceCommitSha === commit;
|
|
18398
|
+
}
|
|
18399
|
+
function pushSource(appId, commit, branch, destination, root, environment) {
|
|
18400
|
+
const remoteHead = git(["ls-remote", "--", destination, "refs/heads/" + branch], root, environment).split(/\s+/)[0];
|
|
18401
|
+
if (remoteHead === commit) return true;
|
|
18402
|
+
if (remoteHead) {
|
|
18403
|
+
git(["fetch", "--no-tags", "--", destination, "refs/heads/" + branch], root, environment);
|
|
18404
|
+
const fetchedHead = git(["rev-parse", "FETCH_HEAD"], root);
|
|
18405
|
+
if (fetchedHead === commit) return true;
|
|
18406
|
+
const parent = git(["show", "-s", "--format=%P", fetchedHead], root);
|
|
18407
|
+
const message = git(["show", "-s", "--format=%B", fetchedHead], root);
|
|
18408
|
+
if (parent === commit && message === "Backify build " + applicationGuid(appId).replaceAll("-", "") + ": " + commit) return true;
|
|
18409
|
+
if (git(["merge-base", commit, fetchedHead], root) !== fetchedHead) throw new Error("Remote history contains other changes. Run backify pull and reconcile them before shipping; nothing was pushed or overwritten.");
|
|
18410
|
+
}
|
|
18411
|
+
git(["push", "--", destination, commit + ":refs/heads/" + branch], root, environment);
|
|
18412
|
+
return false;
|
|
18413
|
+
}
|
|
18414
|
+
async function waitForPreview(call2, prefix, commit, options = {}) {
|
|
18415
|
+
const deadline = Date.now() + (options.timeoutMs ?? 20 * 6e4);
|
|
18416
|
+
const sleep = options.sleep ?? ((delay) => new Promise((resolve24) => setTimeout(resolve24, delay)));
|
|
18417
|
+
do {
|
|
18418
|
+
const deployments = await call2(prefix + "/deployments");
|
|
18419
|
+
const matching = deployments.find((release2) => release2.kind === "preview" && matchesSource(release2, commit) && !options.previousDeploymentIds?.includes(release2.id));
|
|
18420
|
+
if (matching?.status === "failed") throw new Error("Deployment of this source failed. Run backify status and inspect its deployment steps.");
|
|
18421
|
+
const overview = await call2(prefix + "/releases");
|
|
18422
|
+
if (matchesSource(overview.preview, commit) && overview.preview?.status === "succeeded" && overview.preview.healthResult === "healthy") return overview;
|
|
18423
|
+
const build3 = await call2(prefix + "/developer/build?commitSha=" + commit);
|
|
18424
|
+
if ((options.previousBuildRunId === void 0 || build3.runId !== options.previousBuildRunId) && ["failure", "cancelled", "timed_out", "action_required", "startup_failure"].includes(build3.conclusion ?? "")) throw new Error("GitHub build " + build3.conclusion + ". " + (build3.url ?? "Run backify status for details."));
|
|
18425
|
+
await sleep(options.intervalMs ?? 4e3);
|
|
18426
|
+
} while (Date.now() < deadline);
|
|
18427
|
+
throw new Error("Timed out waiting for this commit's healthy preview. Run backify status; no production release was requested.");
|
|
18428
|
+
}
|
|
18429
|
+
async function ship(call2, appId, options = {}) {
|
|
18430
|
+
const root = options.root ?? process.cwd(), prefix = "/apps/" + applicationGuid(appId);
|
|
18431
|
+
const status = await call2(prefix + "/developer");
|
|
18432
|
+
if (!status.repository || !status.branch || !status.workflowReady) throw new Error("The application's repository and build workflow must be connected in Backify.");
|
|
18433
|
+
if (!doctor(root).valid) throw new Error("Run backify doctor and fix project setup before shipping.");
|
|
18434
|
+
if (applicationGuid(projectApplication(root)) !== applicationGuid(appId)) throw new Error("Local application ID does not match the application being shipped.");
|
|
18435
|
+
const actualRoot = git(["rev-parse", "--show-toplevel"], root);
|
|
18436
|
+
if (resolve3(actualRoot).toLowerCase() !== resolve3(root).toLowerCase()) throw new Error("Shipping currently requires the application at the repository root.");
|
|
18437
|
+
const origin = git(["remote", "get-url", "origin"], root);
|
|
18438
|
+
const pushOrigin = git(["remote", "get-url", "--push", "origin"], root);
|
|
18439
|
+
if (repositoryName(origin) !== status.repository.toLowerCase() || repositoryName(pushOrigin) !== status.repository.toLowerCase()) throw new Error("Local repository does not match this Backify application.");
|
|
18440
|
+
if (git(["branch", "--show-current"], root) !== status.branch) throw new Error("Use the connected branch: " + status.branch);
|
|
18441
|
+
if (git(["status", "--porcelain"], root)) throw new Error("Commit application changes before shipping. The CLI never stages or commits unrelated files.");
|
|
18442
|
+
const commit = git(["rev-parse", "HEAD"], root);
|
|
18443
|
+
const report = localReport(true, root);
|
|
18444
|
+
await call2(prefix + "/developer/report", { method: "POST", body: JSON.stringify(report) });
|
|
18445
|
+
if (report.checks.some((check) => check.status === "failed")) throw new Error("Checks failed; nothing was pushed.");
|
|
18446
|
+
if (status.hostingTargetType === "backify-cloud" && !report.outputDirectory && !report.hasDockerfile) throw new Error("Backify Cloud requires a static build containing index.html or a Dockerfile serving port 8080.");
|
|
18447
|
+
if (status.hostingTargetType === "azure" && !report.hasDockerfile) throw new Error("Customer Azure requires a repository-root Dockerfile.");
|
|
18448
|
+
if (git(["status", "--porcelain"], root) || git(["rev-parse", "HEAD"], root) !== commit) throw new Error("Checks changed tracked source. Review and commit those changes before shipping.");
|
|
18449
|
+
const previousDeployments = await call2(prefix + "/deployments");
|
|
18450
|
+
const previousBuild = await call2(prefix + "/developer/build?commitSha=" + commit);
|
|
18451
|
+
const credential = await call2(prefix + "/developer/git-credential", { method: "POST" });
|
|
18452
|
+
if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed; reconnect before shipping.");
|
|
18453
|
+
const environment = gitEnvironment(status.repository, credential.token);
|
|
18454
|
+
const destination = "https://github.com/" + status.repository + ".git";
|
|
18455
|
+
const alreadyPushed = pushSource(appId, commit, status.branch, destination, root, environment);
|
|
18456
|
+
const currentPreview = previousDeployments.find((release2) => release2.kind === "preview" && matchesSource(release2, commit));
|
|
18457
|
+
if (alreadyPushed && (!currentPreview || currentPreview.status === "failed") && !(matchesSource(status.releases.preview, commit) && status.releases.preview?.status === "succeeded")) await call2(prefix + "/deployments/preview", { method: "POST", body: JSON.stringify({ sourceRevision: "latest" }) });
|
|
18458
|
+
const overview = await waitForPreview(call2, prefix, commit, { timeoutMs: options.timeoutMs, previousDeploymentIds: previousDeployments.filter((deployment) => deployment.status === "failed").map((deployment) => deployment.id), previousBuildRunId: previousBuild.runId ?? null });
|
|
18459
|
+
if (overview.preview.commitSha !== commit) {
|
|
18460
|
+
if (git(["status", "--porcelain"], root) || git(["rev-parse", "HEAD"], root) !== commit) throw new Error("Local work changed while the preview built. Review it before synchronizing the build commit.");
|
|
18461
|
+
const refreshed = await call2(prefix + "/developer/git-credential", { method: "POST" });
|
|
18462
|
+
if (refreshed.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed while building.");
|
|
18463
|
+
git(["fetch", "--", destination, "refs/heads/" + status.branch], root, gitEnvironment(status.repository, refreshed.token));
|
|
18464
|
+
git(["merge", "--ff-only", overview.preview.commitSha], root);
|
|
18465
|
+
}
|
|
18466
|
+
return publishPreview(call2, prefix, overview, options);
|
|
18467
|
+
}
|
|
18468
|
+
async function publishPreview(call2, prefix, overview, options = {}) {
|
|
18469
|
+
if (!overview.preview || overview.preview.status !== "succeeded" || overview.preview.healthResult !== "healthy") throw new Error("Only a verified healthy preview can be published.");
|
|
18470
|
+
const preview = overview.preview;
|
|
18471
|
+
if (options.previewOnly) return { status: "preview-ready", url: preview.url };
|
|
18472
|
+
if (overview.policy.requirePreviewTesting && !preview.testedAt) return { status: "testing-required", url: preview.url };
|
|
18473
|
+
if (overview.live?.previewDeploymentId === preview.id && overview.live.status === "succeeded") return { status: "live", url: overview.live.url };
|
|
18474
|
+
const promotion = await call2(prefix + "/deployments/promote", { method: "POST", headers: { "Idempotency-Key": "cli-ship-" + preview.id }, body: JSON.stringify({ previewDeploymentId: preview.id, tested: false }) });
|
|
18475
|
+
if (promotion.status === "awaiting-approval" || promotion.approvalId) return { status: "awaiting-approval", url: preview.url };
|
|
18476
|
+
if (!promotion.operationId) throw new Error("Publication returned no operation to verify. Run backify status.");
|
|
18477
|
+
const deadline = Date.now() + (options.timeoutMs ?? 20 * 6e4);
|
|
18478
|
+
do {
|
|
18479
|
+
const operation = await call2("/operations/" + promotion.operationId);
|
|
18480
|
+
if (operation.status === "failed" || operation.status === "cancelled") throw new Error(operation.errorMessage ?? "Production deployment did not complete.");
|
|
18481
|
+
if (operation.status === "succeeded") {
|
|
18482
|
+
const releases = await call2(prefix + "/releases");
|
|
18483
|
+
if (releases.live?.previewDeploymentId !== preview.id || releases.live.status !== "succeeded" || releases.live.healthResult !== "healthy") throw new Error("Production did not activate this healthy release. Run backify status.");
|
|
18484
|
+
return { status: "live", url: releases.live.url };
|
|
18485
|
+
}
|
|
18486
|
+
await new Promise((resolve24) => setTimeout(resolve24, 4e3));
|
|
18487
|
+
} while (Date.now() < deadline);
|
|
18488
|
+
throw new Error("Publication is still pending. Run backify status; deployment success has not been verified.");
|
|
18489
|
+
}
|
|
18490
|
+
|
|
18491
|
+
// src/browser-auth.ts
|
|
18492
|
+
async function browserAuthenticate(options) {
|
|
18493
|
+
const auth = new URL(apiAddress(options.authUrl));
|
|
18494
|
+
const state = randomBytes(32).toString("base64url");
|
|
18495
|
+
let resolveSession = () => {
|
|
18496
|
+
};
|
|
18497
|
+
let rejectSession = () => {
|
|
18498
|
+
};
|
|
18499
|
+
const completed = new Promise((resolve24, reject) => {
|
|
18500
|
+
resolveSession = resolve24;
|
|
18501
|
+
rejectSession = reject;
|
|
18502
|
+
});
|
|
18503
|
+
let accepted = false;
|
|
18504
|
+
const server = createServer((request, response) => {
|
|
18505
|
+
if (request.headers.origin !== auth.origin) {
|
|
18506
|
+
response.writeHead(403);
|
|
18507
|
+
response.end();
|
|
18508
|
+
return;
|
|
18509
|
+
}
|
|
18510
|
+
response.setHeader("Access-Control-Allow-Origin", auth.origin);
|
|
18511
|
+
response.setHeader("Vary", "Origin");
|
|
18512
|
+
response.setHeader("Cache-Control", "no-store");
|
|
18513
|
+
if (accepted) {
|
|
18514
|
+
response.writeHead(409);
|
|
18515
|
+
response.end();
|
|
18516
|
+
return;
|
|
18517
|
+
}
|
|
18518
|
+
if (request.url !== "/callback") {
|
|
18519
|
+
response.writeHead(404);
|
|
18520
|
+
response.end();
|
|
18521
|
+
return;
|
|
18522
|
+
}
|
|
18523
|
+
if (request.method === "OPTIONS") {
|
|
18524
|
+
response.writeHead(204, { "Access-Control-Allow-Methods": "POST", "Access-Control-Allow-Headers": "content-type", "Access-Control-Allow-Private-Network": "true" });
|
|
18525
|
+
response.end();
|
|
18526
|
+
return;
|
|
18527
|
+
}
|
|
18528
|
+
if (request.method !== "POST" || !request.headers["content-type"]?.startsWith("application/json")) {
|
|
18529
|
+
response.writeHead(405);
|
|
18530
|
+
response.end();
|
|
18531
|
+
return;
|
|
18532
|
+
}
|
|
18533
|
+
let raw = "";
|
|
18534
|
+
request.setEncoding("utf8");
|
|
18535
|
+
request.on("data", (part) => {
|
|
18536
|
+
raw += part;
|
|
18537
|
+
if (raw.length > 65536) request.destroy();
|
|
18538
|
+
});
|
|
18539
|
+
request.on("end", () => {
|
|
18540
|
+
try {
|
|
18541
|
+
const value = JSON.parse(raw);
|
|
18542
|
+
if (value.state !== state || typeof value.token !== "string" || !value.token || value.token.length > 32768 || typeof value.organizationId !== "string" || !/^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(value.organizationId) || options.organizationId && value.organizationId !== options.organizationId) throw new Error("Invalid callback");
|
|
18543
|
+
if (options.applicationId && (value.applicationId !== options.applicationId || !/^bfcli_[0-9a-f]{64}$/i.test(value.token) || typeof value.expiresAt !== "string" || !Number.isFinite(Date.parse(value.expiresAt)) || Date.parse(value.expiresAt) <= Date.now() || Date.parse(value.expiresAt) > Date.now() + 90 * 864e5 + 6e4)) throw new Error("Invalid application session");
|
|
18544
|
+
const session = { token: value.token, organizationId: value.organizationId, apiUrl: options.apiUrl, ...options.applicationId ? { expiresAt: value.expiresAt } : {} };
|
|
18545
|
+
options.save(session);
|
|
18546
|
+
accepted = true;
|
|
18547
|
+
response.writeHead(200, { "Content-Type": "text/plain" });
|
|
18548
|
+
response.end("Backify CLI connected. You can close this tab.");
|
|
18549
|
+
resolveSession(session);
|
|
18550
|
+
} catch {
|
|
18551
|
+
response.writeHead(400);
|
|
18552
|
+
response.end("The CLI could not accept or save this connection. Retry or start a new CLI sign-in.");
|
|
18553
|
+
}
|
|
18554
|
+
});
|
|
18555
|
+
});
|
|
18556
|
+
server.on("error", rejectSession);
|
|
18557
|
+
server.listen(0, "127.0.0.1", () => {
|
|
18558
|
+
try {
|
|
18559
|
+
const address = server.address();
|
|
18560
|
+
if (!address || typeof address === "string") throw new Error("Could not start the local CLI callback.");
|
|
18561
|
+
auth.searchParams.set("cli_callback", "http://127.0.0.1:" + address.port + "/callback");
|
|
18562
|
+
auth.searchParams.set("cli_state", state);
|
|
18563
|
+
if (options.applicationId) auth.searchParams.set("cli_app", options.applicationId);
|
|
18564
|
+
if (options.organizationId) auth.searchParams.set("cli_organization", options.organizationId);
|
|
18565
|
+
auth.searchParams.set("cli_device", JSON.stringify(cliDevice()));
|
|
18566
|
+
options.openBrowser(auth.toString());
|
|
18567
|
+
} catch (error) {
|
|
18568
|
+
rejectSession(error instanceof Error ? error : new Error("Could not open sign-in."));
|
|
18569
|
+
}
|
|
18570
|
+
});
|
|
18571
|
+
const timeout = setTimeout(() => rejectSession(new Error("Backify sign-in timed out. Ask the user to sign in and run backify connect --reauth again.")), options.timeoutMs ?? 5 * 6e4);
|
|
18572
|
+
try {
|
|
18573
|
+
return await completed;
|
|
18574
|
+
} finally {
|
|
18575
|
+
clearTimeout(timeout);
|
|
18576
|
+
server.close();
|
|
18577
|
+
server.closeAllConnections();
|
|
18578
|
+
}
|
|
18579
|
+
}
|
|
18580
|
+
function reconnectingApiClient(session, reconnect, transport = fetch) {
|
|
18581
|
+
let current = session;
|
|
18582
|
+
let pending;
|
|
18583
|
+
return async (path, init) => {
|
|
18584
|
+
const attempted = current;
|
|
18585
|
+
try {
|
|
18586
|
+
return await apiClient(attempted, transport)(path, init);
|
|
18587
|
+
} catch (error) {
|
|
18588
|
+
if (!(error instanceof CliAuthenticationRequired)) throw error;
|
|
18589
|
+
if (current === attempted) {
|
|
18590
|
+
pending ??= reconnect().then((updated) => {
|
|
18591
|
+
current = updated;
|
|
18592
|
+
return updated;
|
|
18593
|
+
}).finally(() => {
|
|
18594
|
+
pending = void 0;
|
|
18595
|
+
});
|
|
18596
|
+
await pending;
|
|
18597
|
+
}
|
|
18598
|
+
return apiClient(current, transport)(path, init);
|
|
18599
|
+
}
|
|
18600
|
+
};
|
|
18601
|
+
}
|
|
18602
|
+
|
|
18603
|
+
// src/local-runtime.ts
|
|
18604
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
18605
|
+
import { chmodSync, existsSync as existsSync5, lstatSync as lstatSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
18606
|
+
import { dirname as dirname5, join as join4, resolve as resolve4, sep } from "node:path";
|
|
18607
|
+
import { homedir } from "node:os";
|
|
18608
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
18609
|
+
|
|
18610
|
+
// src/local-state.ts
|
|
18611
|
+
import { renameSync as renameSync2 } from "node:fs";
|
|
18612
|
+
function replaceLocalFile(source, destination, rename, platform2, wait) {
|
|
18613
|
+
for (let attempt = 0; ; attempt++) {
|
|
18614
|
+
try {
|
|
18615
|
+
rename(source, destination);
|
|
18616
|
+
return;
|
|
18617
|
+
} catch (error) {
|
|
18618
|
+
if (platform2 !== "win32" || attempt >= 4 || !(error instanceof Error) || !("code" in error) || !["EPERM", "EACCES", "EBUSY"].includes(String(error.code))) throw error;
|
|
18619
|
+
wait(10 * 2 ** attempt);
|
|
18620
|
+
}
|
|
18621
|
+
}
|
|
18622
|
+
}
|
|
18623
|
+
function renameLocalFile(source, destination) {
|
|
18624
|
+
replaceLocalFile(source, destination, renameSync2, process.platform, (milliseconds) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds));
|
|
18625
|
+
}
|
|
18626
|
+
|
|
18627
|
+
// src/local-runtime.ts
|
|
18628
|
+
var maxArchiveBytes = 256 * 1024 * 1024;
|
|
18629
|
+
var maxExpandedBytes = 512 * 1024 * 1024;
|
|
18630
|
+
var hash = (bytes) => createHash2("sha256").update(bytes).digest("hex");
|
|
18631
|
+
function runtimeIdentifier(platform2 = process.platform, architecture = process.arch) {
|
|
18632
|
+
const system = { win32: "win", darwin: "osx", linux: "linux" }[platform2];
|
|
18633
|
+
if (!system || !["x64", "arm64"].includes(architecture)) throw new Error("Local runtime supports Windows, macOS and Linux on x64/arm64");
|
|
18634
|
+
return system + "-" + architecture;
|
|
18635
|
+
}
|
|
18636
|
+
function checkedPath(root, filename2) {
|
|
18637
|
+
const target = resolve4(root, filename2);
|
|
18638
|
+
if (target !== resolve4(root) && !target.startsWith(resolve4(root) + sep)) throw new Error("Runtime path escapes its installation");
|
|
18639
|
+
let current = target;
|
|
18640
|
+
while (true) {
|
|
18641
|
+
if (existsSync5(current) && lstatSync3(current).isSymbolicLink()) throw new Error("Runtime installation must not follow links");
|
|
18642
|
+
if (dirname5(current) === current) break;
|
|
18643
|
+
current = dirname5(current);
|
|
18644
|
+
}
|
|
18645
|
+
return target;
|
|
18646
|
+
}
|
|
18647
|
+
function verifyInstallationFiles(root, expected, directory2 = root) {
|
|
18648
|
+
for (const entry of readdirSync3(directory2, { withFileTypes: true })) {
|
|
18649
|
+
const path = checkedPath(root, join4(directory2, entry.name));
|
|
18650
|
+
if (entry.isDirectory()) verifyInstallationFiles(root, expected, path);
|
|
18651
|
+
else if (!entry.isFile() || !Object.hasOwn(expected, path.slice(root.length + 1).split(sep).join("/"))) throw new Error("Installed runtime contains unexpected files; reinstall a reviewed version");
|
|
18652
|
+
}
|
|
18653
|
+
}
|
|
18654
|
+
function installedLocalRuntime(base = join4(homedir(), ".backify")) {
|
|
18655
|
+
const pointer = checkedPath(base, "runtime.json");
|
|
18656
|
+
if (!existsSync5(pointer)) return void 0;
|
|
18657
|
+
const installed = JSON.parse(readFileSync4(pointer, "utf8"));
|
|
18658
|
+
if (!/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(installed.version) || installed.rid !== runtimeIdentifier() || !installed.files || Object.keys(installed.files).length > 3e3) throw new Error("Invalid local runtime installation receipt");
|
|
18659
|
+
const root = checkedPath(base, join4("runtimes", installed.version, installed.rid));
|
|
18660
|
+
verifyInstallationFiles(root, installed.files);
|
|
18661
|
+
for (const [filename2, digest4] of Object.entries(installed.files)) {
|
|
18662
|
+
const path = checkedPath(root, filename2);
|
|
18663
|
+
if (!/^[a-f0-9]{64}$/.test(digest4) || !lstatSync3(path).isFile() || lstatSync3(path).size > maxArchiveBytes || hash(readFileSync4(path)) !== digest4) throw new Error("Installed runtime bytes changed; reinstall a reviewed version");
|
|
18664
|
+
}
|
|
18665
|
+
const executableName = installed.rid.startsWith("win-") ? "Backify.Hcr.exe" : "Backify.Hcr";
|
|
18666
|
+
if (!installed.files["Backify.Hcr.dll"] || !installed.files[executableName]) throw new Error("Runtime archive requires the self-contained HCR executable and DLL");
|
|
18667
|
+
return { path: join4(root, "Backify.Hcr.dll"), executable: join4(root, executableName), version: installed.version, rid: installed.rid };
|
|
18668
|
+
}
|
|
18669
|
+
function installLocalRuntime(archive, options) {
|
|
18670
|
+
if (!/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(options.version) || !/^[a-f0-9]{64}$/.test(options.sha256)) throw new Error("Provide a release version vX.Y.Z and an independently reviewed SHA-256");
|
|
18671
|
+
if (archive.byteLength > maxArchiveBytes || hash(archive) !== options.sha256) throw new Error("Runtime archive digest does not match the reviewed SHA-256");
|
|
18672
|
+
let expanded = 0, count = 0;
|
|
18673
|
+
const files = unzipSync(archive, { filter: (file) => {
|
|
18674
|
+
if (++count > 3e3 || (expanded += file.originalSize) > maxExpandedBytes || file.originalSize > maxArchiveBytes) throw new Error("Runtime archive exceeds extraction limits");
|
|
18675
|
+
if (!file.name || file.name.includes("\\") || file.name.startsWith("/") || file.name.includes(":") || file.name.split("/").some((part) => part === "." || part === "..")) throw new Error("Runtime archive contains an unsafe path");
|
|
18676
|
+
return !file.name.endsWith("/");
|
|
18677
|
+
} });
|
|
18678
|
+
const rid = runtimeIdentifier();
|
|
18679
|
+
const executable = rid.startsWith("win-") ? "Backify.Hcr.exe" : "Backify.Hcr";
|
|
18680
|
+
if (!files["Backify.Hcr.dll"] || !files[executable] || !files["sql/events-v1.sql"] || !files["sql/jobs-v1.sql"]) throw new Error("Runtime archive is missing required host assets");
|
|
18681
|
+
const base = options.base ?? join4(homedir(), ".backify");
|
|
18682
|
+
const parent = checkedPath(base, join4("runtimes", options.version));
|
|
18683
|
+
mkdirSync3(parent, { recursive: true, mode: 448 });
|
|
18684
|
+
const destination = checkedPath(parent, rid);
|
|
18685
|
+
const receipt = { version: options.version, rid, files: Object.fromEntries(Object.entries(files).map(([name2, bytes]) => [name2, hash(bytes)])) };
|
|
18686
|
+
if (!existsSync5(destination)) {
|
|
18687
|
+
const staging = checkedPath(parent, ".install-" + randomUUID2());
|
|
18688
|
+
mkdirSync3(staging, { mode: 448 });
|
|
18689
|
+
for (const [name2, bytes] of Object.entries(files)) {
|
|
18690
|
+
const filename2 = checkedPath(staging, name2);
|
|
18691
|
+
mkdirSync3(dirname5(filename2), { recursive: true, mode: 448 });
|
|
18692
|
+
writeFileSync5(filename2, bytes, { flag: "wx", mode: name2 === executable ? 448 : 384 });
|
|
18693
|
+
}
|
|
18694
|
+
if (process.platform !== "win32") chmodSync(join4(staging, executable), 448);
|
|
18695
|
+
renameLocalFile(staging, destination);
|
|
18696
|
+
} else {
|
|
18697
|
+
verifyInstallationFiles(destination, receipt.files);
|
|
18698
|
+
for (const [name2, digest4] of Object.entries(receipt.files)) if (hash(readFileSync4(checkedPath(destination, name2))) !== digest4) throw new Error("Runtime version already exists with different bytes; never overwrite an installation");
|
|
18699
|
+
}
|
|
18700
|
+
const pointer = checkedPath(base, "runtime.json");
|
|
18701
|
+
const temporary = checkedPath(base, ".runtime-" + randomUUID2() + ".json");
|
|
18702
|
+
writeFileSync5(temporary, JSON.stringify(receipt), { flag: "wx", mode: 384 });
|
|
18703
|
+
renameLocalFile(temporary, pointer);
|
|
18704
|
+
return installedLocalRuntime(base);
|
|
18705
|
+
}
|
|
18706
|
+
async function downloadLocalRuntime(version) {
|
|
18707
|
+
if (!/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(version)) throw new Error("Provide a release version vX.Y.Z");
|
|
18708
|
+
let url = new URL("https://github.com/frejnorling/backify/releases/download/runtime-" + version + "/backify-hcr-" + runtimeIdentifier() + ".zip");
|
|
18709
|
+
for (let redirects = 0; redirects < 5; redirects++) {
|
|
18710
|
+
if (url.protocol !== "https:" || !["github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com"].includes(url.hostname) || url.username || url.password) throw new Error("Runtime download left the approved release hosts");
|
|
18711
|
+
const response = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(6e4) });
|
|
18712
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
18713
|
+
url = new URL(response.headers.get("location") ?? "", url);
|
|
18714
|
+
continue;
|
|
18715
|
+
}
|
|
18716
|
+
if (!response.ok || !response.body) throw new Error("The requested runtime release is unavailable");
|
|
18717
|
+
const chunks = [];
|
|
18718
|
+
let size = 0;
|
|
18719
|
+
for await (const chunk of response.body) {
|
|
18720
|
+
size += chunk.byteLength;
|
|
18721
|
+
if (size > maxArchiveBytes) throw new Error("Runtime archive exceeds its size limit");
|
|
18722
|
+
chunks.push(chunk);
|
|
18723
|
+
}
|
|
18724
|
+
return Buffer.concat(chunks);
|
|
18725
|
+
}
|
|
18726
|
+
throw new Error("Too many runtime download redirects");
|
|
18727
|
+
}
|
|
18728
|
+
function localPrerequisites() {
|
|
18729
|
+
const runtime = installedLocalRuntime();
|
|
18730
|
+
const directory2 = process.env.BACKIFY_LOCAL_POSTGRES_BIN ?? (process.platform === "win32" ? join4(process.env.ProgramFiles ?? "C:/Program Files", "PostgreSQL", "17", "bin") : void 0);
|
|
18731
|
+
const checks = ["initdb", "pg_ctl", "psql"].map((name2) => {
|
|
18732
|
+
const executable = directory2 ? join4(directory2, name2 + (process.platform === "win32" ? ".exe" : "")) : name2;
|
|
18733
|
+
const result = spawnSync3(executable, ["--version"], { windowsHide: true, timeout: 1e4, encoding: "utf8" });
|
|
18734
|
+
return { name: name2, available: result.status === 0 };
|
|
18735
|
+
});
|
|
18736
|
+
return { rid: runtimeIdentifier(), runtime: runtime ? { installed: true, version: runtime.version } : { installed: false }, postgres: checks, ready: !!runtime && checks.every((check) => check.available) };
|
|
18737
|
+
}
|
|
18738
|
+
|
|
18739
|
+
// src/workload.ts
|
|
18740
|
+
import { build as build2 } from "esbuild";
|
|
18741
|
+
import { createHash as createHash14 } from "node:crypto";
|
|
18742
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync19, realpathSync as realpathSync6, readdirSync as readdirSync6, writeFileSync as writeFileSync11 } from "node:fs";
|
|
18743
|
+
import { dirname as dirname13, isAbsolute as isAbsolute14, join as join13, relative as relative8, resolve as resolve18 } from "node:path";
|
|
18744
|
+
|
|
18745
|
+
// src/local-development.ts
|
|
18746
|
+
import { context as context2 } from "esbuild";
|
|
18747
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
18748
|
+
import { randomBytes as randomBytes6, randomUUID as randomUUID9, createHash as createHash13 } from "node:crypto";
|
|
18749
|
+
import { existsSync as existsSync11, lstatSync as lstatSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync18, realpathSync as realpathSync5, writeFileSync as writeFileSync10, unlinkSync as unlinkSync4 } from "node:fs";
|
|
18750
|
+
import { resolve as resolve17, join as join12, relative as relative7, isAbsolute as isAbsolute13, dirname as dirname12 } from "node:path";
|
|
18751
|
+
|
|
18752
|
+
// ../analyzer/dist/index.js
|
|
18753
|
+
import ts2 from "typescript";
|
|
18754
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync4, existsSync as existsSync6 } from "node:fs";
|
|
18755
|
+
import { join as join5, extname } from "node:path";
|
|
18756
|
+
|
|
18757
|
+
// ../analyzer/dist/authentication.js
|
|
18758
|
+
import ts from "typescript";
|
|
18759
|
+
function analyzeAuthentication(files) {
|
|
18760
|
+
const providers = /* @__PURE__ */ new Set();
|
|
18761
|
+
const dynamicFiles = /* @__PURE__ */ new Set();
|
|
18762
|
+
let used = false;
|
|
18763
|
+
for (const file of files) {
|
|
18764
|
+
const sourcePath = file.path.replaceAll(String.fromCharCode(92), "/");
|
|
18765
|
+
const source = ts.createSourceFile(sourcePath, file.content, ts.ScriptTarget.Latest, true, file.path.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
18766
|
+
const program = ts.createProgram([sourcePath], { noLib: true, noResolve: true, allowJs: true }, {
|
|
18767
|
+
getSourceFile: (path) => path === sourcePath ? source : void 0,
|
|
18768
|
+
getDefaultLibFileName: () => "",
|
|
18769
|
+
writeFile: () => {
|
|
18770
|
+
},
|
|
18771
|
+
getCurrentDirectory: () => "/",
|
|
18772
|
+
getDirectories: () => [],
|
|
18773
|
+
fileExists: (path) => path === sourcePath,
|
|
18774
|
+
readFile: (path) => path === sourcePath ? file.content : void 0,
|
|
18775
|
+
getCanonicalFileName: (path) => path,
|
|
18776
|
+
useCaseSensitiveFileNames: () => true,
|
|
18777
|
+
getNewLine: () => String.fromCharCode(10)
|
|
18778
|
+
});
|
|
18779
|
+
const checker = program.getTypeChecker();
|
|
18780
|
+
const authSymbols = /* @__PURE__ */ new Set(), namespaceSymbols = /* @__PURE__ */ new Set(), rootSymbols = /* @__PURE__ */ new Set(), factorySymbols = /* @__PURE__ */ new Set();
|
|
18781
|
+
const contains = (symbols, node) => {
|
|
18782
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
18783
|
+
return symbol !== void 0 && symbols.has(symbol);
|
|
18784
|
+
};
|
|
18785
|
+
for (const statement of source.statements) {
|
|
18786
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !["@backifydev/sdk", "@backifydev/sdk/server"].includes(statement.moduleSpecifier.text) || statement.importClause?.isTypeOnly)
|
|
18787
|
+
continue;
|
|
18788
|
+
const bindings = statement.importClause?.namedBindings;
|
|
18789
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
18790
|
+
const symbol = checker.getSymbolAtLocation(bindings.name);
|
|
18791
|
+
if (symbol)
|
|
18792
|
+
namespaceSymbols.add(symbol);
|
|
18793
|
+
}
|
|
18794
|
+
if (bindings && ts.isNamedImports(bindings))
|
|
18795
|
+
for (const item of bindings.elements) {
|
|
18796
|
+
if (item.isTypeOnly)
|
|
18797
|
+
continue;
|
|
18798
|
+
const imported = (item.propertyName ?? item.name).text, symbol = checker.getSymbolAtLocation(item.name);
|
|
18799
|
+
if (!symbol)
|
|
18800
|
+
continue;
|
|
18801
|
+
if (imported === "auth")
|
|
18802
|
+
authSymbols.add(symbol);
|
|
18803
|
+
if (imported === "backify")
|
|
18804
|
+
rootSymbols.add(symbol);
|
|
18805
|
+
if (imported === "createServerClient")
|
|
18806
|
+
factorySymbols.add(symbol);
|
|
18807
|
+
}
|
|
18808
|
+
}
|
|
18809
|
+
const isNamespace = (expression) => ts.isIdentifier(expression) && contains(namespaceSymbols, expression);
|
|
18810
|
+
const isFactory = (expression) => ts.isIdentifier(expression) ? contains(factorySymbols, expression) : ts.isPropertyAccessExpression(expression) && expression.name.text === "createServerClient" && isNamespace(expression.expression);
|
|
18811
|
+
const isRoot = (expression) => ts.isIdentifier(expression) ? contains(rootSymbols, expression) : ts.isCallExpression(expression) ? isFactory(expression.expression) : ts.isPropertyAccessExpression(expression) && expression.name.text === "backify" && isNamespace(expression.expression);
|
|
18812
|
+
const isAuth = (expression) => ts.isIdentifier(expression) ? contains(authSymbols, expression) : ts.isPropertyAccessExpression(expression) && expression.name.text === "auth" && (isRoot(expression.expression) || isNamespace(expression.expression));
|
|
18813
|
+
const register = (symbols, identifier2) => {
|
|
18814
|
+
const symbol = checker.getSymbolAtLocation(identifier2);
|
|
18815
|
+
if (symbol)
|
|
18816
|
+
symbols.add(symbol);
|
|
18817
|
+
};
|
|
18818
|
+
const collectRoots = (node) => {
|
|
18819
|
+
if (ts.isVariableDeclaration(node) && node.initializer) {
|
|
18820
|
+
if (ts.isIdentifier(node.name)) {
|
|
18821
|
+
if (isRoot(node.initializer))
|
|
18822
|
+
register(rootSymbols, node.name);
|
|
18823
|
+
if (isAuth(node.initializer))
|
|
18824
|
+
register(authSymbols, node.name);
|
|
18825
|
+
} else if (ts.isObjectBindingPattern(node.name)) {
|
|
18826
|
+
for (const binding of node.name.elements) {
|
|
18827
|
+
if (!ts.isIdentifier(binding.name) || binding.dotDotDotToken || binding.initializer)
|
|
18828
|
+
continue;
|
|
18829
|
+
const property = binding.propertyName ?? binding.name;
|
|
18830
|
+
if (!ts.isIdentifier(property) && !ts.isStringLiteral(property))
|
|
18831
|
+
continue;
|
|
18832
|
+
if (property.text === "auth" && (isRoot(node.initializer) || isNamespace(node.initializer)))
|
|
18833
|
+
register(authSymbols, binding.name);
|
|
18834
|
+
if (property.text === "backify" && isNamespace(node.initializer))
|
|
18835
|
+
register(rootSymbols, binding.name);
|
|
18836
|
+
if (property.text === "createServerClient" && isNamespace(node.initializer))
|
|
18837
|
+
register(factorySymbols, binding.name);
|
|
18838
|
+
}
|
|
18839
|
+
}
|
|
18840
|
+
}
|
|
18841
|
+
ts.forEachChild(node, collectRoots);
|
|
18842
|
+
};
|
|
18843
|
+
let previousSize = -1;
|
|
18844
|
+
while (previousSize !== authSymbols.size + rootSymbols.size + factorySymbols.size) {
|
|
18845
|
+
previousSize = authSymbols.size + rootSymbols.size + factorySymbols.size;
|
|
18846
|
+
collectRoots(source);
|
|
18847
|
+
}
|
|
18848
|
+
const visit = (node) => {
|
|
18849
|
+
if (ts.isIdentifier(node) && contains(authSymbols, node) && !ts.isImportSpecifier(node.parent) && !ts.isTypeReferenceNode(node.parent) && !ts.isTypeQueryNode(node.parent) && !ts.isExportSpecifier(node.parent))
|
|
18850
|
+
used = true;
|
|
18851
|
+
if (ts.isPropertyAccessExpression(node) && isAuth(node))
|
|
18852
|
+
used = true;
|
|
18853
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "signIn" && isAuth(node.expression.expression)) {
|
|
18854
|
+
const options = node.arguments[0];
|
|
18855
|
+
if (options) {
|
|
18856
|
+
const property = ts.isObjectLiteralExpression(options) ? options.properties.find((value) => ts.isPropertyAssignment(value) && (ts.isIdentifier(value.name) || ts.isStringLiteral(value.name)) && value.name.text === "provider") : void 0;
|
|
18857
|
+
if (property && ts.isPropertyAssignment(property) && ts.isStringLiteral(property.initializer) && (!ts.isObjectLiteralExpression(options) || !options.properties.some(ts.isSpreadAssignment)))
|
|
18858
|
+
providers.add(property.initializer.text);
|
|
18859
|
+
else
|
|
18860
|
+
dynamicFiles.add(file.path);
|
|
18861
|
+
}
|
|
18862
|
+
}
|
|
18863
|
+
ts.forEachChild(node, visit);
|
|
18864
|
+
};
|
|
18865
|
+
visit(source);
|
|
18866
|
+
}
|
|
18867
|
+
return { used, providers: [...providers].sort(), dynamicFiles: [...dynamicFiles].sort() };
|
|
18868
|
+
}
|
|
18869
|
+
|
|
18870
|
+
// ../analyzer/dist/index.js
|
|
18871
|
+
var bypass = { "@azure/storage-blob": { capability: "storage", replacement: "storage" }, "@aws-sdk/client-s3": { capability: "storage", replacement: "storage" }, "firebase-admin": { capability: "database", replacement: "db" }, "@supabase/supabase-js": { capability: "database", replacement: "db" }, "@azure/service-bus": { capability: "jobs", replacement: "jobs" } };
|
|
18872
|
+
function analyzeSources(files, options = {}) {
|
|
18873
|
+
const used = /* @__PURE__ */ new Set(), diagnostics = [];
|
|
18874
|
+
for (const file of files)
|
|
18875
|
+
analyzeFile(file, used, diagnostics);
|
|
18876
|
+
if (used.has("http"))
|
|
18877
|
+
used.add("events");
|
|
18878
|
+
const authentication = analyzeAuthentication(files);
|
|
18879
|
+
if (authentication.used)
|
|
18880
|
+
used.add("auth");
|
|
18881
|
+
else
|
|
18882
|
+
used.delete("auth");
|
|
18883
|
+
const capabilities = Object.fromEntries([...used].sort().map((name2) => [name2, { version: CAPABILITY_REGISTRY[name2].version }]));
|
|
18884
|
+
if (capabilities.auth && (authentication.providers.length || authentication.dynamicFiles.length))
|
|
18885
|
+
capabilities.auth.configuration = { providers: authentication.providers, dynamicProviders: authentication.dynamicFiles.length > 0 };
|
|
18886
|
+
for (const file of authentication.dynamicFiles)
|
|
18887
|
+
diagnostics.push({ severity: "warning", code: "dynamic-auth-provider", file, docs: CAPABILITY_REGISTRY.auth.docs, message: "Declare every possible authentication provider in backify.yaml; dynamic provider selection cannot be provisioned from source alone." });
|
|
18888
|
+
for (const provider of authentication.providers)
|
|
18889
|
+
if (!["entra", "google", "email"].includes(provider))
|
|
18890
|
+
diagnostics.push({ severity: "error", code: "unsupported-auth-provider", docs: CAPABILITY_REGISTRY.auth.docs, message: "Authentication provider is not implemented: " + provider });
|
|
18891
|
+
return { schemaVersion: 1, applicationId: options.applicationId ?? "unknown", commit: options.commit ?? "unknown", sdkVersion: options.sdkVersion ?? "unknown", capabilities, diagnostics };
|
|
18892
|
+
}
|
|
18893
|
+
function analyzeFile(file, used, diagnostics) {
|
|
18894
|
+
const source = ts2.createSourceFile(file.path, file.content, ts2.ScriptTarget.Latest, true, file.path.endsWith("x") ? ts2.ScriptKind.TSX : ts2.ScriptKind.TS), named = /* @__PURE__ */ new Map(), namespaces = /* @__PURE__ */ new Set(), roots = /* @__PURE__ */ new Set(), factories = /* @__PURE__ */ new Set();
|
|
18895
|
+
source.forEachChild((node) => {
|
|
18896
|
+
if (!ts2.isImportDeclaration(node) || !ts2.isStringLiteral(node.moduleSpecifier))
|
|
18897
|
+
return;
|
|
18898
|
+
const module = node.moduleSpecifier.text;
|
|
18899
|
+
if (bypass[module]) {
|
|
18900
|
+
const rule = bypass[module];
|
|
18901
|
+
diagnostics.push({ severity: "warning", code: "direct-provider-sdk", file: file.path, docs: CAPABILITY_REGISTRY[rule.capability].docs, message: `Direct provider dependency '${module}' detected. Use import { ${rule.replacement} } from "@backifydev/sdk" so Backify can provision and bind ${rule.capability}.` });
|
|
18902
|
+
}
|
|
18903
|
+
if (module !== "@backifydev/sdk" && module !== "@backifydev/sdk/server" || !node.importClause)
|
|
18904
|
+
return;
|
|
18905
|
+
const bindings = node.importClause.namedBindings;
|
|
18906
|
+
if (!bindings)
|
|
18907
|
+
return;
|
|
18908
|
+
if (ts2.isNamespaceImport(bindings))
|
|
18909
|
+
namespaces.add(bindings.name.text);
|
|
18910
|
+
else if (ts2.isNamedImports(bindings))
|
|
18911
|
+
for (const element of bindings.elements) {
|
|
18912
|
+
const imported = (element.propertyName ?? element.name).text, local = element.name.text;
|
|
18913
|
+
if (imported === "createServerClient")
|
|
18914
|
+
factories.add(local);
|
|
18915
|
+
else if (imported === "backify")
|
|
18916
|
+
roots.add(local);
|
|
18917
|
+
else {
|
|
18918
|
+
const cap = SDK_IMPORT_CAPABILITIES[imported];
|
|
18919
|
+
if (cap)
|
|
18920
|
+
named.set(local, cap);
|
|
18921
|
+
}
|
|
18922
|
+
}
|
|
18923
|
+
});
|
|
18924
|
+
const visit = (node) => {
|
|
18925
|
+
if (ts2.isVariableDeclaration(node) && ts2.isIdentifier(node.name) && node.initializer && ts2.isCallExpression(node.initializer) && ts2.isIdentifier(node.initializer.expression) && factories.has(node.initializer.expression.text))
|
|
18926
|
+
roots.add(node.name.text);
|
|
18927
|
+
if (ts2.isCallExpression(node) && ts2.isIdentifier(node.expression) && named.get(node.expression.text) === "jobs" && node.arguments.some((argument) => ts2.isObjectLiteralExpression(argument) && argument.properties.some((property) => ts2.isPropertyAssignment(property) && property.name.getText(source) === "schedule")))
|
|
18928
|
+
used.add("scheduler");
|
|
18929
|
+
if (ts2.isIdentifier(node)) {
|
|
18930
|
+
const parent = node.parent;
|
|
18931
|
+
if (named.has(node.text) && !ts2.isImportSpecifier(parent) && isValueUse(node))
|
|
18932
|
+
used.add(named.get(node.text));
|
|
18933
|
+
}
|
|
18934
|
+
if (ts2.isPropertyAccessExpression(node)) {
|
|
18935
|
+
if (ts2.isIdentifier(node.expression)) {
|
|
18936
|
+
if (namespaces.has(node.expression.text)) {
|
|
18937
|
+
const cap = SDK_IMPORT_CAPABILITIES[node.name.text];
|
|
18938
|
+
if (cap)
|
|
18939
|
+
used.add(cap);
|
|
18940
|
+
}
|
|
18941
|
+
if (roots.has(node.expression.text)) {
|
|
18942
|
+
const cap = SDK_IMPORT_CAPABILITIES[node.name.text];
|
|
18943
|
+
if (cap)
|
|
18944
|
+
used.add(cap);
|
|
18945
|
+
}
|
|
18946
|
+
}
|
|
18947
|
+
if (node.name.text === "schedule" && rootCapability(node.expression, named, namespaces, roots) === "jobs")
|
|
18948
|
+
used.add("scheduler");
|
|
18949
|
+
}
|
|
18950
|
+
ts2.forEachChild(node, visit);
|
|
18951
|
+
};
|
|
18952
|
+
visit(source);
|
|
18953
|
+
}
|
|
18954
|
+
function isValueUse(node) {
|
|
18955
|
+
const p = node.parent;
|
|
18956
|
+
return !(ts2.isImportSpecifier(p) || ts2.isExportSpecifier(p) || ts2.isTypeReferenceNode(p) || ts2.isTypeQueryNode(p));
|
|
18957
|
+
}
|
|
18958
|
+
function rootCapability(node, named, namespaces, roots) {
|
|
18959
|
+
if (ts2.isIdentifier(node))
|
|
18960
|
+
return named.get(node.text);
|
|
18961
|
+
if (ts2.isPropertyAccessExpression(node) && ts2.isIdentifier(node.expression)) {
|
|
18962
|
+
if (namespaces.has(node.expression.text) || roots.has(node.expression.text))
|
|
18963
|
+
return SDK_IMPORT_CAPABILITIES[node.name.text];
|
|
18964
|
+
}
|
|
18965
|
+
return void 0;
|
|
18966
|
+
}
|
|
18967
|
+
function repositorySources(root) {
|
|
18968
|
+
const result = [];
|
|
18969
|
+
const walk = (dir) => {
|
|
18970
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
18971
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".backify" || entry.name.startsWith(".git"))
|
|
18972
|
+
continue;
|
|
18973
|
+
const path = join5(dir, entry.name);
|
|
18974
|
+
if (entry.isDirectory())
|
|
18975
|
+
walk(path);
|
|
18976
|
+
else if ([".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs"].includes(extname(path)))
|
|
18977
|
+
result.push({ path, content: readFileSync5(path, "utf8") });
|
|
18978
|
+
}
|
|
18979
|
+
};
|
|
18980
|
+
if (existsSync6(root))
|
|
18981
|
+
walk(root);
|
|
18982
|
+
return result;
|
|
18983
|
+
}
|
|
18984
|
+
|
|
18985
|
+
// src/local-inspection.ts
|
|
18986
|
+
function createLocalDataInspector(runtime, options) {
|
|
18273
18987
|
const tableName = (value) => typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]{0,62}$/.test(value) && !/^__backify/i.test(value);
|
|
18274
18988
|
const bucketName = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,100}$/.test(value);
|
|
18275
18989
|
if (typeof window !== "undefined" || options.buckets.length > 100 || options.buckets.some((name2) => !bucketName(name2)) || new Set(options.buckets).size !== options.buckets.length) throw new Error("invalid_local_inspection");
|
|
@@ -18472,9 +19186,9 @@ function createLocalNativeEvents(local, native) {
|
|
|
18472
19186
|
|
|
18473
19187
|
// src/local-job-executor.ts
|
|
18474
19188
|
import { spawn as spawn2 } from "node:child_process";
|
|
18475
|
-
import { createHash as
|
|
18476
|
-
import { lstatSync as
|
|
18477
|
-
import { isAbsolute as
|
|
19189
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
19190
|
+
import { lstatSync as lstatSync11, readFileSync as readFileSync12 } from "node:fs";
|
|
19191
|
+
import { isAbsolute as isAbsolute8, join as join8, relative as relative3, resolve as resolve11 } from "node:path";
|
|
18478
19192
|
|
|
18479
19193
|
// ../sdk/dist/schedules.js
|
|
18480
19194
|
var import_cron_parser = __toESM(require_dist2(), 1);
|
|
@@ -18685,28 +19399,28 @@ function createApplicationAuthProvider(source, options) {
|
|
|
18685
19399
|
}
|
|
18686
19400
|
|
|
18687
19401
|
// ../sdk/dist/application-auth-state.js
|
|
18688
|
-
import { createHash as
|
|
18689
|
-
import { closeSync, constants, fstatSync, fsyncSync, lstatSync as
|
|
18690
|
-
import { dirname as
|
|
19402
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
|
|
19403
|
+
import { closeSync, constants, fstatSync, fsyncSync, lstatSync as lstatSync4, openSync, readSync, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
19404
|
+
import { dirname as dirname6, isAbsolute as isAbsolute2, parse, resolve as resolve5, sep as sep2 } from "node:path";
|
|
18691
19405
|
function unavailable2() {
|
|
18692
19406
|
throw new ApplicationAuthenticationError("application_identity_unavailable");
|
|
18693
19407
|
}
|
|
18694
19408
|
function safePath(filename2) {
|
|
18695
|
-
if (!
|
|
19409
|
+
if (!isAbsolute2(filename2) || resolve5(filename2) !== filename2 || filename2 === parse(filename2).root)
|
|
18696
19410
|
unavailable2();
|
|
18697
19411
|
if (process.platform === "win32" && filename2.slice(parse(filename2).root.length).split(sep2).some((component) => component.includes(":") || /[. ]$/.test(component) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(component)))
|
|
18698
19412
|
unavailable2();
|
|
18699
|
-
const directory2 =
|
|
19413
|
+
const directory2 = dirname6(filename2);
|
|
18700
19414
|
let ancestor = directory2;
|
|
18701
19415
|
for (; ; ) {
|
|
18702
|
-
const metadata =
|
|
19416
|
+
const metadata = lstatSync4(ancestor);
|
|
18703
19417
|
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
18704
19418
|
unavailable2();
|
|
18705
19419
|
if (ancestor === parse(ancestor).root)
|
|
18706
19420
|
break;
|
|
18707
|
-
ancestor =
|
|
19421
|
+
ancestor = dirname6(ancestor);
|
|
18708
19422
|
}
|
|
18709
|
-
const parent =
|
|
19423
|
+
const parent = lstatSync4(directory2);
|
|
18710
19424
|
if (process.platform !== "win32" && ((parent.mode & 63) !== 0 || parent.uid !== process.getuid?.()))
|
|
18711
19425
|
unavailable2();
|
|
18712
19426
|
}
|
|
@@ -18717,7 +19431,7 @@ function safeFile(metadata) {
|
|
|
18717
19431
|
function syncDirectory(filename2) {
|
|
18718
19432
|
if (process.platform === "win32")
|
|
18719
19433
|
return;
|
|
18720
|
-
const descriptor2 = openSync(
|
|
19434
|
+
const descriptor2 = openSync(dirname6(filename2), constants.O_RDONLY | constants.O_DIRECTORY);
|
|
18721
19435
|
try {
|
|
18722
19436
|
fsyncSync(descriptor2);
|
|
18723
19437
|
} finally {
|
|
@@ -18725,7 +19439,7 @@ function syncDirectory(filename2) {
|
|
|
18725
19439
|
}
|
|
18726
19440
|
}
|
|
18727
19441
|
function readState(filename2) {
|
|
18728
|
-
const metadata =
|
|
19442
|
+
const metadata = lstatSync4(filename2);
|
|
18729
19443
|
safeFile(metadata);
|
|
18730
19444
|
if (metadata.size > 2048)
|
|
18731
19445
|
unavailable2();
|
|
@@ -18758,30 +19472,30 @@ function readState(filename2) {
|
|
|
18758
19472
|
}
|
|
18759
19473
|
}
|
|
18760
19474
|
function persist(filename2, state) {
|
|
18761
|
-
const temporary = filename2 + "." +
|
|
19475
|
+
const temporary = filename2 + "." + randomUUID3() + ".tmp";
|
|
18762
19476
|
const descriptor2 = openSync(temporary, "wx", 384);
|
|
18763
19477
|
try {
|
|
18764
|
-
|
|
19478
|
+
writeFileSync6(descriptor2, JSON.stringify(state));
|
|
18765
19479
|
fsyncSync(descriptor2);
|
|
18766
19480
|
} finally {
|
|
18767
19481
|
closeSync(descriptor2);
|
|
18768
19482
|
}
|
|
18769
|
-
|
|
19483
|
+
renameSync3(temporary, filename2);
|
|
18770
19484
|
syncDirectory(filename2);
|
|
18771
19485
|
}
|
|
18772
19486
|
function checkpoint(filename2, trust, initialize2) {
|
|
18773
19487
|
try {
|
|
18774
|
-
if (!
|
|
19488
|
+
if (!isAbsolute2(filename2))
|
|
18775
19489
|
unavailable2();
|
|
18776
|
-
filename2 =
|
|
19490
|
+
filename2 = resolve5(filename2);
|
|
18777
19491
|
safePath(filename2);
|
|
18778
19492
|
const lock = filename2 + ".lock";
|
|
18779
|
-
const next = { version: 1, scope: trust.scope, revision: trust.revision, digest:
|
|
19493
|
+
const next = { version: 1, scope: trust.scope, revision: trust.revision, digest: createHash3("sha256").update(trust.content).digest("hex") };
|
|
18780
19494
|
if (!initialize2) {
|
|
18781
19495
|
const previous = readState(filename2);
|
|
18782
19496
|
if (previous.scope.organizationId === next.scope.organizationId && previous.scope.applicationId === next.scope.applicationId && previous.scope.environmentId === next.scope.environmentId && previous.revision === next.revision && previous.digest === next.digest) {
|
|
18783
19497
|
try {
|
|
18784
|
-
|
|
19498
|
+
lstatSync4(lock);
|
|
18785
19499
|
} catch (error) {
|
|
18786
19500
|
if (error.code === "ENOENT")
|
|
18787
19501
|
return;
|
|
@@ -18792,7 +19506,7 @@ function checkpoint(filename2, trust, initialize2) {
|
|
|
18792
19506
|
}
|
|
18793
19507
|
const descriptor2 = openSync(lock, "wx", 384);
|
|
18794
19508
|
try {
|
|
18795
|
-
|
|
19509
|
+
writeFileSync6(descriptor2, "backify-application-auth-state-v1");
|
|
18796
19510
|
fsyncSync(descriptor2);
|
|
18797
19511
|
} finally {
|
|
18798
19512
|
closeSync(descriptor2);
|
|
@@ -18802,7 +19516,7 @@ function checkpoint(filename2, trust, initialize2) {
|
|
|
18802
19516
|
try {
|
|
18803
19517
|
if (initialize2) {
|
|
18804
19518
|
try {
|
|
18805
|
-
|
|
19519
|
+
lstatSync4(filename2);
|
|
18806
19520
|
unavailable2();
|
|
18807
19521
|
} catch (error) {
|
|
18808
19522
|
if (error.code !== "ENOENT")
|
|
@@ -18839,18 +19553,18 @@ function createPersistentApplicationAuthProvider(source, options) {
|
|
|
18839
19553
|
}
|
|
18840
19554
|
|
|
18841
19555
|
// ../sdk/dist/application-auth-sync.js
|
|
18842
|
-
import { createHash as
|
|
18843
|
-
import { closeSync as closeSync2, constants as constants2, fsyncSync as fsyncSync2, lstatSync as
|
|
18844
|
-
import { dirname as
|
|
19556
|
+
import { createHash as createHash4, createPublicKey as createPublicKey2, randomUUID as randomUUID4, verify as verify2 } from "node:crypto";
|
|
19557
|
+
import { closeSync as closeSync2, constants as constants2, fsyncSync as fsyncSync2, lstatSync as lstatSync5, openSync as openSync2, readFileSync as readFileSync6, renameSync as renameSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
19558
|
+
import { dirname as dirname7, isAbsolute as isAbsolute3, parse as parse2, resolve as resolve6, sep as sep3 } from "node:path";
|
|
18845
19559
|
var maximumBytes = 512 * 1024;
|
|
18846
19560
|
var identifier = (value) => typeof value === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value) && value !== "00000000-0000-0000-0000-000000000000";
|
|
18847
19561
|
function unavailable3() {
|
|
18848
19562
|
throw new ApplicationAuthenticationError("application_identity_unavailable");
|
|
18849
19563
|
}
|
|
18850
19564
|
function filename(value) {
|
|
18851
|
-
if (typeof value !== "string" || /[\x00-\x1f\x7f]/.test(value) || !
|
|
19565
|
+
if (typeof value !== "string" || /[\x00-\x1f\x7f]/.test(value) || !isAbsolute3(value))
|
|
18852
19566
|
unavailable3();
|
|
18853
|
-
const result =
|
|
19567
|
+
const result = resolve6(value);
|
|
18854
19568
|
if (result === parse2(result).root || process.platform === "win32" && result.slice(parse2(result).root.length).split(sep3).some((component) => component.includes(":") || /[. ]$/.test(component) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(component)))
|
|
18855
19569
|
unavailable3();
|
|
18856
19570
|
return result;
|
|
@@ -18879,28 +19593,28 @@ function prepare(options) {
|
|
|
18879
19593
|
function planApplicationAuthSynchronization(options) {
|
|
18880
19594
|
try {
|
|
18881
19595
|
const prepared = prepare(options);
|
|
18882
|
-
return { schemaVersion: 1, status: "planned", scope: structuredClone(options.expectedScope), agentId: options.agentId, endpoint: prepared.endpoint.href, managementKeySha256:
|
|
19596
|
+
return { schemaVersion: 1, status: "planned", scope: structuredClone(options.expectedScope), agentId: options.agentId, endpoint: prepared.endpoint.href, managementKeySha256: createHash4("sha256").update(prepared.key.export({ type: "spki", format: "der" })).digest("hex"), trustFile: prepared.trustFile, stateFile: prepared.stateFile, requiresInitializedOperatorJournal: true, workloadCredentialsRequired: false, deploymentReady: false };
|
|
18883
19597
|
} catch {
|
|
18884
19598
|
return unavailable3();
|
|
18885
19599
|
}
|
|
18886
19600
|
}
|
|
18887
19601
|
function directory(filename2, privateState) {
|
|
18888
|
-
const parent =
|
|
18889
|
-
for (let ancestor = parent; ; ancestor =
|
|
18890
|
-
const metadata2 =
|
|
19602
|
+
const parent = dirname7(filename2);
|
|
19603
|
+
for (let ancestor = parent; ; ancestor = dirname7(ancestor)) {
|
|
19604
|
+
const metadata2 = lstatSync5(ancestor);
|
|
18891
19605
|
if (!metadata2.isDirectory() || metadata2.isSymbolicLink())
|
|
18892
19606
|
unavailable3();
|
|
18893
19607
|
if (ancestor === parse2(ancestor).root)
|
|
18894
19608
|
break;
|
|
18895
19609
|
}
|
|
18896
|
-
const metadata =
|
|
19610
|
+
const metadata = lstatSync5(parent);
|
|
18897
19611
|
if (process.platform !== "win32" && (metadata.uid !== process.getuid?.() || (metadata.mode & (privateState ? 63 : 18)) !== 0))
|
|
18898
19612
|
unavailable3();
|
|
18899
19613
|
}
|
|
18900
19614
|
function syncDirectory2(filename2) {
|
|
18901
19615
|
if (process.platform === "win32")
|
|
18902
19616
|
return;
|
|
18903
|
-
const descriptor2 = openSync2(
|
|
19617
|
+
const descriptor2 = openSync2(dirname7(filename2), constants2.O_RDONLY | constants2.O_DIRECTORY);
|
|
18904
19618
|
try {
|
|
18905
19619
|
fsyncSync2(descriptor2);
|
|
18906
19620
|
} finally {
|
|
@@ -18909,10 +19623,10 @@ function syncDirectory2(filename2) {
|
|
|
18909
19623
|
}
|
|
18910
19624
|
function currentPublicFile(filename2) {
|
|
18911
19625
|
try {
|
|
18912
|
-
const metadata =
|
|
19626
|
+
const metadata = lstatSync5(filename2);
|
|
18913
19627
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > 131072 || process.platform !== "win32" && (metadata.uid !== process.getuid?.() || (metadata.mode & 18) !== 0))
|
|
18914
19628
|
unavailable3();
|
|
18915
|
-
const bytes =
|
|
19629
|
+
const bytes = readFileSync6(filename2);
|
|
18916
19630
|
if (bytes.length > 131072)
|
|
18917
19631
|
unavailable3();
|
|
18918
19632
|
return new TextDecoder("utf8", { fatal: true }).decode(bytes);
|
|
@@ -18979,7 +19693,7 @@ async function synchronizeApplicationAuthTrustWithDigest(options) {
|
|
|
18979
19693
|
const lock = prepared.stateFile + ".delivery.lock";
|
|
18980
19694
|
const descriptor2 = openSync2(lock, "wx", 384);
|
|
18981
19695
|
try {
|
|
18982
|
-
|
|
19696
|
+
writeFileSync7(descriptor2, "backify-application-auth-delivery-v1");
|
|
18983
19697
|
fsyncSync2(descriptor2);
|
|
18984
19698
|
} finally {
|
|
18985
19699
|
closeSync2(descriptor2);
|
|
@@ -18990,19 +19704,19 @@ async function synchronizeApplicationAuthTrustWithDigest(options) {
|
|
|
18990
19704
|
const current = currentPublicFile(prepared.trustFile);
|
|
18991
19705
|
createPersistentApplicationAuthProvider(trust, { expectedScope: options.expectedScope, stateFile: prepared.stateFile, now: options.now });
|
|
18992
19706
|
const content = JSON.stringify(trust);
|
|
18993
|
-
const trustDigest =
|
|
19707
|
+
const trustDigest = createHash4("sha256").update(content).digest("hex");
|
|
18994
19708
|
if (current === content)
|
|
18995
19709
|
return { status: "unchanged", revision: trust.revision, validUntil: trust.validUntil, trustDigest };
|
|
18996
19710
|
writePending = true;
|
|
18997
|
-
const temporary = prepared.trustFile + "." +
|
|
19711
|
+
const temporary = prepared.trustFile + "." + randomUUID4() + ".tmp";
|
|
18998
19712
|
const output = openSync2(temporary, "wx", process.platform === "win32" ? 384 : 292);
|
|
18999
19713
|
try {
|
|
19000
|
-
|
|
19714
|
+
writeFileSync7(output, content);
|
|
19001
19715
|
fsyncSync2(output);
|
|
19002
19716
|
} finally {
|
|
19003
19717
|
closeSync2(output);
|
|
19004
19718
|
}
|
|
19005
|
-
|
|
19719
|
+
renameSync4(temporary, prepared.trustFile);
|
|
19006
19720
|
syncDirectory2(prepared.trustFile);
|
|
19007
19721
|
writePending = false;
|
|
19008
19722
|
return { status: "updated", revision: trust.revision, validUntil: trust.validUntil, trustDigest };
|
|
@@ -19579,16 +20293,16 @@ function jsonValue(value) {
|
|
|
19579
20293
|
|
|
19580
20294
|
// src/local-services.ts
|
|
19581
20295
|
import { spawn, execFile } from "node:child_process";
|
|
19582
|
-
import { createHash as
|
|
19583
|
-
import { createServer } from "node:net";
|
|
19584
|
-
import { existsSync as
|
|
19585
|
-
import { basename, dirname as
|
|
20296
|
+
import { createHash as createHash6, generateKeyPairSync, randomBytes as randomBytes3, randomUUID as randomUUID6, sign } from "node:crypto";
|
|
20297
|
+
import { createServer as createServer2 } from "node:net";
|
|
20298
|
+
import { existsSync as existsSync8, lstatSync as lstatSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync8, readdirSync as readdirSync5, realpathSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync8 } from "node:fs";
|
|
20299
|
+
import { basename as basename2, dirname as dirname9, isAbsolute as isAbsolute4, join as join7, relative, resolve as resolve7 } from "node:path";
|
|
19586
20300
|
|
|
19587
20301
|
// src/local-job-services.ts
|
|
19588
|
-
import { createHash as
|
|
19589
|
-
import { existsSync as
|
|
20302
|
+
import { createHash as createHash5, randomBytes as randomBytes2, randomUUID as randomUUID5 } from "node:crypto";
|
|
20303
|
+
import { existsSync as existsSync7, lstatSync as lstatSync6, readFileSync as readFileSync7 } from "node:fs";
|
|
19590
20304
|
import { isIP } from "node:net";
|
|
19591
|
-
import { dirname as
|
|
20305
|
+
import { dirname as dirname8, join as join6 } from "node:path";
|
|
19592
20306
|
function fields3(value, names) {
|
|
19593
20307
|
return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === names.length && names.every((name2) => Object.hasOwn(value, name2));
|
|
19594
20308
|
}
|
|
@@ -19639,12 +20353,12 @@ function validLocalJobState(value, otherIds) {
|
|
|
19639
20353
|
function prepareLocalJobService(hcrPath, retained, artifacts, applyMigrations) {
|
|
19640
20354
|
const requested = validateLocalJobArtifacts(artifacts);
|
|
19641
20355
|
const readSchema = (name2) => {
|
|
19642
|
-
const filename2 =
|
|
20356
|
+
const filename2 = join6(dirname8(hcrPath), "sql", name2), info = existsSync7(filename2) ? lstatSync6(filename2) : void 0;
|
|
19643
20357
|
if (!info?.isFile() || info.isSymbolicLink() || info.size > 65536) throw new Error("Native jobs require the reviewed jobs-v1.sql and jobs-producer-v1.sql assets beside the HCR host");
|
|
19644
|
-
return
|
|
20358
|
+
return readFileSync7(filename2, "utf8");
|
|
19645
20359
|
};
|
|
19646
20360
|
const base = readSchema("jobs-v1.sql"), producer = readSchema("jobs-producer-v1.sql");
|
|
19647
|
-
const schemaDigests = { base:
|
|
20361
|
+
const schemaDigests = { base: createHash5("sha256").update(base).digest("hex"), producer: createHash5("sha256").update(producer).digest("hex") };
|
|
19648
20362
|
if (retained && (retained.schemaDigests.base !== schemaDigests.base || retained.schemaDigests.producer !== schemaDigests.producer)) throw new Error("Native job schema changed; preserve its database and plan an explicit migration");
|
|
19649
20363
|
if (!retained?.initialized && !applyMigrations) throw new Error("Review both HCR job SQL assets and pass --apply-migrations for explicit native job schema approval");
|
|
19650
20364
|
const retainedArtifacts = retained ? validateLocalJobArtifacts(retained.artifacts) : [];
|
|
@@ -19653,20 +20367,20 @@ function prepareLocalJobService(hcrPath, retained, artifacts, applyMigrations) {
|
|
|
19653
20367
|
if (previous && JSON.stringify(previous) !== JSON.stringify(artifact)) throw new Error("Retained local job artifact metadata cannot change; preserve the original pins and policies");
|
|
19654
20368
|
if (!previous) retainedArtifacts.push(artifact);
|
|
19655
20369
|
}
|
|
19656
|
-
const state = { ...retained ?? { id:
|
|
20370
|
+
const state = { ...retained ?? { id: randomUUID5(), password: randomBytes2(32).toString("hex"), initialized: false }, schemaDigests, artifacts: validateLocalJobArtifacts(retainedArtifacts) };
|
|
19657
20371
|
return { state, base, producer };
|
|
19658
20372
|
}
|
|
19659
20373
|
|
|
19660
20374
|
// src/local-services.ts
|
|
19661
20375
|
function writePrivate(filename2, value) {
|
|
19662
20376
|
if (isLink(filename2)) throw new Error("Local service files must not be symbolic links");
|
|
19663
|
-
const temporary = filename2 + "." +
|
|
19664
|
-
|
|
20377
|
+
const temporary = filename2 + "." + randomUUID6() + ".tmp";
|
|
20378
|
+
writeFileSync8(temporary, value, { flag: "wx", mode: 384 });
|
|
19665
20379
|
renameLocalFile(temporary, filename2);
|
|
19666
20380
|
}
|
|
19667
20381
|
function isLink(filename2) {
|
|
19668
20382
|
try {
|
|
19669
|
-
return
|
|
20383
|
+
return lstatSync7(filename2).isSymbolicLink();
|
|
19670
20384
|
} catch (error) {
|
|
19671
20385
|
if (error.code === "ENOENT") return false;
|
|
19672
20386
|
throw error;
|
|
@@ -19674,9 +20388,9 @@ function isLink(filename2) {
|
|
|
19674
20388
|
}
|
|
19675
20389
|
function assertLocalStatePath(root, filename2) {
|
|
19676
20390
|
const base = realpathSync(root);
|
|
19677
|
-
for (let current =
|
|
20391
|
+
for (let current = resolve7(filename2); current !== base; current = resolve7(current, "..")) {
|
|
19678
20392
|
const part = relative(base, current);
|
|
19679
|
-
if (part === ".." || part.startsWith(".." + (process.platform === "win32" ? "\\" : "/")) ||
|
|
20393
|
+
if (part === ".." || part.startsWith(".." + (process.platform === "win32" ? "\\" : "/")) || isAbsolute4(part)) throw new Error("Local service state must stay inside the repository");
|
|
19680
20394
|
if (isLink(current)) throw new Error("Local service paths must not contain links");
|
|
19681
20395
|
}
|
|
19682
20396
|
}
|
|
@@ -19687,22 +20401,22 @@ function stateValid(value) {
|
|
|
19687
20401
|
return value?.version === 1 && value.scope && [value.scope.organizationId, value.scope.applicationId, value.scope.environmentId, value.databaseId, value.storageId].every((identifier2) => typeof identifier2 === "string" && uuid5.test(identifier2)) && Number.isSafeInteger(value.configurationVersion) && value.configurationVersion >= 0 && /^[a-f0-9]{64}$/.test(value.adminPassword) && /^[a-f0-9]{64}$/.test(value.databasePassword) && (value.retainedDatabasePort === void 0 || Number.isInteger(value.retainedDatabasePort) && value.retainedDatabasePort > 0 && value.retainedDatabasePort <= 65535);
|
|
19688
20402
|
}
|
|
19689
20403
|
function acquireLocalLease(root, relativePath2 = ".backify/dev/development.lock") {
|
|
19690
|
-
const filename2 =
|
|
20404
|
+
const filename2 = join7(root, relativePath2);
|
|
19691
20405
|
assertLocalStatePath(root, filename2);
|
|
19692
|
-
|
|
19693
|
-
const owner = JSON.stringify({ version: 1, pid: process.pid, id:
|
|
20406
|
+
mkdirSync4(resolve7(filename2, ".."), { recursive: true, mode: 448 });
|
|
20407
|
+
const owner = JSON.stringify({ version: 1, pid: process.pid, id: randomUUID6() });
|
|
19694
20408
|
try {
|
|
19695
|
-
|
|
20409
|
+
writeFileSync8(filename2, owner, { flag: "wx", mode: 384 });
|
|
19696
20410
|
} catch {
|
|
19697
20411
|
throw new Error("Local development already owns this repository, or a stale lifecycle lock requires explicit recovery");
|
|
19698
20412
|
}
|
|
19699
20413
|
return () => {
|
|
19700
|
-
if (
|
|
20414
|
+
if (existsSync8(filename2) && readFileSync8(filename2, "utf8") === owner) unlinkSync3(filename2);
|
|
19701
20415
|
};
|
|
19702
20416
|
}
|
|
19703
20417
|
async function allocateLocalPort(requested) {
|
|
19704
20418
|
if (requested !== void 0 && (!Number.isInteger(requested) || requested < 0 || requested > 65535)) throw new Error("Local service ports must be 0-65535");
|
|
19705
|
-
const server =
|
|
20419
|
+
const server = createServer2();
|
|
19706
20420
|
return new Promise((resolvePort, reject) => {
|
|
19707
20421
|
server.once("error", reject);
|
|
19708
20422
|
server.listen(requested ?? 0, "127.0.0.1", () => {
|
|
@@ -19718,7 +20432,7 @@ async function allocateLocalPort(requested) {
|
|
|
19718
20432
|
}
|
|
19719
20433
|
function command(executable, args2, options = {}) {
|
|
19720
20434
|
const failure2 = () => new Error("Local service command failed; verify installed tools, port availability and local state permissions");
|
|
19721
|
-
if (
|
|
20435
|
+
if (basename2(executable).replace(/\.exe$/i, "") === "pg_ctl") return new Promise((resolveOutput, reject) => {
|
|
19722
20436
|
const child = spawn(executable, args2, { ...options, windowsHide: true, stdio: "ignore" });
|
|
19723
20437
|
const timer = setTimeout(() => {
|
|
19724
20438
|
child.kill("SIGKILL");
|
|
@@ -19740,29 +20454,29 @@ function command(executable, args2, options = {}) {
|
|
|
19740
20454
|
});
|
|
19741
20455
|
}
|
|
19742
20456
|
function postgresExecutable(name2, bin) {
|
|
19743
|
-
const directory2 = bin ?? process.env.BACKIFY_LOCAL_POSTGRES_BIN ?? (process.platform === "win32" ?
|
|
19744
|
-
return directory2 ?
|
|
20457
|
+
const directory2 = bin ?? process.env.BACKIFY_LOCAL_POSTGRES_BIN ?? (process.platform === "win32" ? join7(process.env.ProgramFiles ?? "C:/Program Files", "PostgreSQL", "17", "bin") : void 0);
|
|
20458
|
+
return directory2 ? join7(directory2, name2 + (process.platform === "win32" ? ".exe" : "")) : name2;
|
|
19745
20459
|
}
|
|
19746
20460
|
function localMigrationFiles(root, directory2) {
|
|
19747
20461
|
if (directory2 === void 0) return [];
|
|
19748
20462
|
if (typeof directory2 !== "string") throw new Error("Local migrations require a repository-relative directory");
|
|
19749
|
-
const folder =
|
|
20463
|
+
const folder = resolve7(root, directory2);
|
|
19750
20464
|
assertLocalStatePath(root, folder);
|
|
19751
|
-
if (!
|
|
19752
|
-
const entries =
|
|
20465
|
+
if (!existsSync8(folder)) throw new Error("Declared migration directory does not exist");
|
|
20466
|
+
const entries = readdirSync5(folder);
|
|
19753
20467
|
const filenames = entries.filter((entry) => entry.endsWith(".sql")).sort();
|
|
19754
20468
|
if (filenames.length > 100 || entries.some((entry) => !entry.endsWith(".sql"))) throw new Error("Migrations require at most 100 flat .sql files");
|
|
19755
20469
|
let total = 0;
|
|
19756
20470
|
return filenames.map((name2) => {
|
|
19757
20471
|
if (!/^[A-Za-z0-9_.-]{1,150}\.sql$/.test(name2)) throw new Error("Invalid migration filename");
|
|
19758
|
-
const filename2 =
|
|
20472
|
+
const filename2 = join7(folder, name2);
|
|
19759
20473
|
assertLocalStatePath(root, filename2);
|
|
19760
|
-
const info =
|
|
20474
|
+
const info = lstatSync7(filename2);
|
|
19761
20475
|
if (!info.isFile() || info.size > 1024 * 1024 || (total += info.size) > 4 * 1024 * 1024) throw new Error("Migration SQL exceeds supported limits");
|
|
19762
|
-
const sql =
|
|
20476
|
+
const sql = readFileSync8(filename2, "utf8");
|
|
19763
20477
|
if (!sql.trim()) throw new Error("Migration SQL must not be empty");
|
|
19764
20478
|
validateLocalMigrationSql(sql);
|
|
19765
|
-
return { name: name2, digest:
|
|
20479
|
+
return { name: name2, digest: createHash6("sha256").update(sql).digest("hex"), sql };
|
|
19766
20480
|
});
|
|
19767
20481
|
}
|
|
19768
20482
|
function validateLocalMigrationSql(sql) {
|
|
@@ -19836,19 +20550,19 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19836
20550
|
const reviewedScope = options.jobIdentityScope;
|
|
19837
20551
|
if (reviewedScope !== void 0 && (!jobs || !manifest.capabilities.auth || manifest.capabilities.auth.enabled === false || !reviewedScope || Object.keys(reviewedScope).sort().join(",") !== "applicationId,environmentId,organizationId" || Object.values(reviewedScope).some((value) => typeof value !== "string" || value.length !== 36 || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value) || value === "00000000-0000-0000-0000-000000000000"))) throw new Error("Native worker identity scope requires reviewed jobs and enabled auth");
|
|
19838
20552
|
const installed = options.hcrPath || process.env.BACKIFY_LOCAL_HCR_PATH ? void 0 : installedLocalRuntime();
|
|
19839
|
-
const hcrPath =
|
|
19840
|
-
if (!
|
|
19841
|
-
const directory2 =
|
|
20553
|
+
const hcrPath = resolve7(options.hcrPath ?? process.env.BACKIFY_LOCAL_HCR_PATH ?? installed?.path ?? join7(root, "apps/hcr/bin/Debug/net9.0/Backify.Hcr.dll"));
|
|
20554
|
+
if (!existsSync8(hcrPath) || !lstatSync7(hcrPath).isFile() || !hcrPath.endsWith(".dll")) throw new Error("Full-fidelity mode requires a built Backify.Hcr.dll; set --hcr-path or BACKIFY_LOCAL_HCR_PATH");
|
|
20555
|
+
const directory2 = join7(root, ".backify", "dev", "services");
|
|
19842
20556
|
assertLocalStatePath(root, directory2);
|
|
19843
|
-
|
|
20557
|
+
mkdirSync4(directory2, { recursive: true, mode: 448 });
|
|
19844
20558
|
assertLocalStatePath(root, directory2);
|
|
19845
|
-
const statePath =
|
|
20559
|
+
const statePath = join7(directory2, "host.json");
|
|
19846
20560
|
assertLocalStatePath(root, statePath);
|
|
19847
20561
|
let existing;
|
|
19848
|
-
if (
|
|
19849
|
-
if (
|
|
20562
|
+
if (existsSync8(statePath)) {
|
|
20563
|
+
if (lstatSync7(statePath).size > 16384) throw new Error("Local ownership state exceeds supported limits");
|
|
19850
20564
|
try {
|
|
19851
|
-
existing = JSON.parse(
|
|
20565
|
+
existing = JSON.parse(readFileSync8(statePath, "utf8"));
|
|
19852
20566
|
} catch {
|
|
19853
20567
|
throw new Error("Invalid local service ownership state; preserve the data and repair explicitly");
|
|
19854
20568
|
}
|
|
@@ -19856,31 +20570,31 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19856
20570
|
}
|
|
19857
20571
|
const state = existing ?? {
|
|
19858
20572
|
version: 1,
|
|
19859
|
-
scope: reviewedScope ? structuredClone(reviewedScope) : { organizationId:
|
|
19860
|
-
databaseId:
|
|
19861
|
-
storageId:
|
|
20573
|
+
scope: reviewedScope ? structuredClone(reviewedScope) : { organizationId: randomUUID6(), applicationId: randomUUID6(), environmentId: randomUUID6() },
|
|
20574
|
+
databaseId: randomUUID6(),
|
|
20575
|
+
storageId: randomUUID6(),
|
|
19862
20576
|
configurationVersion: 0,
|
|
19863
|
-
adminPassword:
|
|
19864
|
-
databasePassword:
|
|
20577
|
+
adminPassword: randomBytes3(32).toString("hex"),
|
|
20578
|
+
databasePassword: randomBytes3(32).toString("hex")
|
|
19865
20579
|
};
|
|
19866
20580
|
if (!stateValid(state)) throw new Error("Invalid local service ownership state; preserve the data and repair explicitly");
|
|
19867
20581
|
if (reviewedScope && ["organizationId", "applicationId", "environmentId"].some((name2) => state.scope[name2] !== reviewedScope[name2])) throw new Error("Reviewed native worker identity scope conflicts with retained local ownership; preserve data and use a separate reviewed workspace");
|
|
19868
20582
|
let eventSchema;
|
|
19869
20583
|
if (events) {
|
|
19870
|
-
const filename2 =
|
|
19871
|
-
if (!
|
|
19872
|
-
eventSchema =
|
|
19873
|
-
const digest4 =
|
|
20584
|
+
const filename2 = join7(dirname9(hcrPath), "sql", "events-v1.sql");
|
|
20585
|
+
if (!existsSync8(filename2) || lstatSync7(filename2).isSymbolicLink() || !lstatSync7(filename2).isFile() || lstatSync7(filename2).size > 65536) throw new Error("Native events require the reviewed events-v1.sql asset beside the HCR host.");
|
|
20586
|
+
eventSchema = readFileSync8(filename2, "utf8");
|
|
20587
|
+
const digest4 = createHash6("sha256").update(eventSchema).digest("hex");
|
|
19874
20588
|
if (state.events && state.events.schemaDigest !== digest4) throw new Error("Native event schema changed; preserve its database and plan an explicit migration.");
|
|
19875
20589
|
if (!state.events?.initialized && !options.applyMigrations) throw new Error("Review the HCR events-v1.sql asset and pass --apply-migrations for explicit native event schema approval.");
|
|
19876
|
-
state.events ??= { id:
|
|
20590
|
+
state.events ??= { id: randomUUID6(), password: randomBytes3(32).toString("hex"), schemaDigest: digest4, initialized: false };
|
|
19877
20591
|
}
|
|
19878
20592
|
const jobSchema = jobs ? prepareLocalJobService(hcrPath, state.jobs, options.jobArtifacts, options.applyMigrations) : void 0;
|
|
19879
20593
|
if (jobSchema) state.jobs = jobSchema.state;
|
|
19880
20594
|
const storageNames = manifest.capabilities.storage?.buckets === void 0 ? ["default"] : manifest.capabilities.storage.buckets.map((bucket) => bucket.name);
|
|
19881
20595
|
state.storageBindings ??= { default: state.storageId };
|
|
19882
20596
|
if (typeof state.storageBindings !== "object" || state.storageBindings === null || Array.isArray(state.storageBindings) || Object.entries(state.storageBindings).some(([name2, identifier2]) => !/^[A-Za-z0-9_-]{1,100}$/.test(name2) || typeof identifier2 !== "string" || !/^[0-9a-f-]{36}$/i.test(identifier2))) throw new Error("Invalid local storage binding ownership state");
|
|
19883
|
-
for (const name2 of storageNames) state.storageBindings[name2] ??=
|
|
20597
|
+
for (const name2 of storageNames) state.storageBindings[name2] ??= randomUUID6();
|
|
19884
20598
|
if (Object.keys(state.storageBindings).length > 100) throw new Error("Local storage ownership history exceeds 100 bindings; preserve the data and plan an explicit migration");
|
|
19885
20599
|
if (Buffer.byteLength(JSON.stringify({ ...state, configurationVersion: Number.MAX_SAFE_INTEGER, retainedDatabasePort: 65535 }), "utf8") > 16384) throw new Error("Local ownership history exceeds supported limits; preserve state and plan an explicit migration");
|
|
19886
20600
|
const migrations = database ? localMigrationFiles(root, manifest.capabilities.database?.migrations) : [];
|
|
@@ -19888,7 +20602,7 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19888
20602
|
const databasePort = database || events || jobs ? await allocateLocalPort(options.databasePort) : void 0;
|
|
19889
20603
|
if (databasePort !== void 0) state.retainedDatabasePort = databasePort;
|
|
19890
20604
|
if (databasePort === hcrPort) throw new Error("HCR and PostgreSQL require separate ports");
|
|
19891
|
-
const cluster =
|
|
20605
|
+
const cluster = join7(directory2, "postgres");
|
|
19892
20606
|
assertLocalStatePath(root, cluster);
|
|
19893
20607
|
const release2 = acquireLocalLease(root, ".backify/dev/services/lifecycle.lock");
|
|
19894
20608
|
let postgresStarted = false;
|
|
@@ -19904,7 +20618,7 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19904
20618
|
}
|
|
19905
20619
|
};
|
|
19906
20620
|
const runtimeUrl = "http://127.0.0.1:" + hcrPort;
|
|
19907
|
-
const tokenPath =
|
|
20621
|
+
const tokenPath = join7(directory2, "workload-token");
|
|
19908
20622
|
const stop = () => stopping ??= (async () => {
|
|
19909
20623
|
if (timer) clearInterval(timer);
|
|
19910
20624
|
if (snapshotTimer) clearInterval(snapshotTimer);
|
|
@@ -19923,9 +20637,9 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19923
20637
|
}
|
|
19924
20638
|
notify("hcr", "stopped");
|
|
19925
20639
|
try {
|
|
19926
|
-
if (postgresStarted &&
|
|
20640
|
+
if (postgresStarted && existsSync8(join7(cluster, "postmaster.pid"))) await command(postgresExecutable("pg_ctl", options.postgresBin), ["-D", cluster, "-m", "fast", "-w", "stop"]);
|
|
19927
20641
|
if (postgresStarted) notify("postgres", "stopped");
|
|
19928
|
-
if (
|
|
20642
|
+
if (existsSync8(tokenPath)) unlinkSync3(tokenPath);
|
|
19929
20643
|
} finally {
|
|
19930
20644
|
release2();
|
|
19931
20645
|
}
|
|
@@ -19935,28 +20649,28 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19935
20649
|
writePrivate(statePath, JSON.stringify(state));
|
|
19936
20650
|
if (database || events || jobs) {
|
|
19937
20651
|
notify("postgres", "starting");
|
|
19938
|
-
if (!
|
|
19939
|
-
const passwordPath =
|
|
20652
|
+
if (!existsSync8(join7(cluster, "PG_VERSION"))) {
|
|
20653
|
+
const passwordPath = join7(directory2, "init-password");
|
|
19940
20654
|
writePrivate(passwordPath, state.adminPassword);
|
|
19941
20655
|
try {
|
|
19942
20656
|
await command(postgresExecutable("initdb", options.postgresBin), ["-D", cluster, "-U", "backify_admin", "--auth-host=scram-sha-256", "--auth-local=scram-sha-256", "--pwfile=" + passwordPath, "--no-locale", "--encoding=UTF8"]);
|
|
19943
20657
|
} finally {
|
|
19944
|
-
if (
|
|
20658
|
+
if (existsSync8(passwordPath)) unlinkSync3(passwordPath);
|
|
19945
20659
|
}
|
|
19946
20660
|
}
|
|
19947
|
-
if (
|
|
20661
|
+
if (existsSync8(join7(cluster, "postmaster.pid"))) throw new Error("A retained local PostgreSQL server is still running; stop or recover that exact owned cluster before restarting dev");
|
|
19948
20662
|
postgresStarted = true;
|
|
19949
|
-
await command(postgresExecutable("pg_ctl", options.postgresBin), ["-D", cluster, "-l",
|
|
20663
|
+
await command(postgresExecutable("pg_ctl", options.postgresBin), ["-D", cluster, "-l", join7(directory2, "postgres.log"), "-o", "-h 127.0.0.1 -p " + databasePort, "-w", "start"]);
|
|
19950
20664
|
notify("postgres", "ready");
|
|
19951
20665
|
const sql = (query, application = false) => command(postgresExecutable("psql", options.postgresBin), ["-h", "127.0.0.1", "-p", String(databasePort), "-U", application ? "backify_local" : "backify_admin", "-d", application ? "backify_local" : "postgres", "-X", "-q", "-t", "-A", "-v", "ON_ERROR_STOP=1", "-c", query], { env: { ...process.env, PGPASSWORD: application ? state.databasePassword : state.adminPassword } });
|
|
19952
20666
|
if (database) {
|
|
19953
20667
|
if ((await sql("SELECT count(*) FROM pg_roles WHERE rolname='backify_local'")).trim() === "0") {
|
|
19954
|
-
const filename2 =
|
|
20668
|
+
const filename2 = join7(directory2, "role-" + randomUUID6() + ".sql");
|
|
19955
20669
|
writePrivate(filename2, "CREATE ROLE backify_local LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '" + state.databasePassword + "';");
|
|
19956
20670
|
try {
|
|
19957
20671
|
await command(postgresExecutable("psql", options.postgresBin), ["-h", "127.0.0.1", "-p", String(databasePort), "-U", "backify_admin", "-d", "postgres", "-X", "-q", "-v", "ON_ERROR_STOP=1", "-f", filename2], { env: { ...process.env, PGPASSWORD: state.adminPassword } });
|
|
19958
20672
|
} finally {
|
|
19959
|
-
if (
|
|
20673
|
+
if (existsSync8(filename2)) unlinkSync3(filename2);
|
|
19960
20674
|
}
|
|
19961
20675
|
}
|
|
19962
20676
|
if ((await sql("SELECT count(*) FROM pg_database WHERE datname='backify_local'")).trim() === "0") await sql("CREATE DATABASE backify_local OWNER backify_local");
|
|
@@ -19971,12 +20685,12 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19971
20685
|
notify("migrations", "starting");
|
|
19972
20686
|
if (pending.length) await sql("CREATE TABLE IF NOT EXISTS public.__backify_migrations (name text PRIMARY KEY, digest text NOT NULL, applied_at timestamptz NOT NULL DEFAULT now())", true);
|
|
19973
20687
|
for (const migration of pending) {
|
|
19974
|
-
const filename2 =
|
|
20688
|
+
const filename2 = join7(directory2, "apply-" + randomUUID6() + ".sql");
|
|
19975
20689
|
writePrivate(filename2, "BEGIN;\n" + migration.sql + "\n;\nINSERT INTO public.__backify_migrations(name,digest) VALUES ('" + migration.name + "','" + migration.digest + "');\nCOMMIT;\n");
|
|
19976
20690
|
try {
|
|
19977
20691
|
await command(postgresExecutable("psql", options.postgresBin), ["-h", "127.0.0.1", "-p", String(databasePort), "-U", "backify_local", "-d", "backify_local", "-X", "-q", "-v", "ON_ERROR_STOP=1", "-f", filename2], { env: { ...process.env, PGPASSWORD: state.databasePassword } });
|
|
19978
20692
|
} finally {
|
|
19979
|
-
if (
|
|
20693
|
+
if (existsSync8(filename2)) unlinkSync3(filename2);
|
|
19980
20694
|
}
|
|
19981
20695
|
applied.push(migration.name);
|
|
19982
20696
|
}
|
|
@@ -19987,20 +20701,20 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19987
20701
|
const eventSql = (query) => command(postgresExecutable("psql", options.postgresBin), ["-h", "127.0.0.1", "-p", String(databasePort), "-U", "backify_events", "-d", "backify_events", "-X", "-q", "-t", "-A", "-v", "ON_ERROR_STOP=1", "-c", query], { env: { ...process.env, PGPASSWORD: eventState.password } });
|
|
19988
20702
|
if (!eventState.initialized) {
|
|
19989
20703
|
if ((await sql("SELECT count(*) FROM pg_database WHERE datname='backify_events'")).trim() !== "0" || (await sql("SELECT count(*) FROM pg_roles WHERE rolname='backify_events'")).trim() !== "0") throw new Error("Uninitialized native event ownership conflicts with retained state; preserve it and repair explicitly.");
|
|
19990
|
-
const roleFile =
|
|
20704
|
+
const roleFile = join7(directory2, "event-role-" + randomUUID6() + ".sql");
|
|
19991
20705
|
writePrivate(roleFile, "CREATE ROLE backify_events LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '" + eventState.password + "';");
|
|
19992
20706
|
try {
|
|
19993
20707
|
await command(postgresExecutable("psql", options.postgresBin), ["-h", "127.0.0.1", "-p", String(databasePort), "-U", "backify_admin", "-d", "postgres", "-X", "-q", "-v", "ON_ERROR_STOP=1", "-f", roleFile], { env: { ...process.env, PGPASSWORD: state.adminPassword } });
|
|
19994
20708
|
} finally {
|
|
19995
|
-
if (
|
|
20709
|
+
if (existsSync8(roleFile)) unlinkSync3(roleFile);
|
|
19996
20710
|
}
|
|
19997
20711
|
await sql("CREATE DATABASE backify_events OWNER backify_events");
|
|
19998
|
-
const schemaFile =
|
|
20712
|
+
const schemaFile = join7(directory2, "event-schema-" + randomUUID6() + ".sql");
|
|
19999
20713
|
writePrivate(schemaFile, eventSchema);
|
|
20000
20714
|
try {
|
|
20001
20715
|
await command(postgresExecutable("psql", options.postgresBin), ["-h", "127.0.0.1", "-p", String(databasePort), "-U", "backify_events", "-d", "backify_events", "-X", "-q", "-v", "ON_ERROR_STOP=1", "-f", schemaFile], { env: { ...process.env, PGPASSWORD: eventState.password } });
|
|
20002
20716
|
} finally {
|
|
20003
|
-
if (
|
|
20717
|
+
if (existsSync8(schemaFile)) unlinkSync3(schemaFile);
|
|
20004
20718
|
}
|
|
20005
20719
|
eventState.initialized = true;
|
|
20006
20720
|
writePrivate(statePath, JSON.stringify(state));
|
|
@@ -20014,21 +20728,21 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
20014
20728
|
const adminSql = (query, databaseName = "postgres") => jobCommand("backify_admin", state.adminPassword, databaseName, ["-c", query]);
|
|
20015
20729
|
if (!jobState.initialized) {
|
|
20016
20730
|
if ((await adminSql("SELECT count(*) FROM pg_database WHERE datname='backify_jobs'")).trim() !== "0" || (await adminSql("SELECT count(*) FROM pg_roles WHERE rolname='backify_jobs'")).trim() !== "0") throw new Error("Uninitialized native job ownership conflicts with retained state; preserve it and repair explicitly");
|
|
20017
|
-
const roleFile =
|
|
20731
|
+
const roleFile = join7(directory2, "job-role-" + randomUUID6() + ".sql");
|
|
20018
20732
|
writePrivate(roleFile, "CREATE ROLE backify_jobs LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD '" + jobState.password + "';");
|
|
20019
20733
|
try {
|
|
20020
20734
|
await jobCommand("backify_admin", state.adminPassword, "postgres", ["-f", roleFile]);
|
|
20021
20735
|
} finally {
|
|
20022
|
-
if (
|
|
20736
|
+
if (existsSync8(roleFile)) unlinkSync3(roleFile);
|
|
20023
20737
|
}
|
|
20024
20738
|
await adminSql("CREATE DATABASE backify_jobs OWNER backify_admin");
|
|
20025
20739
|
for (const schema of [jobSchema.base, jobSchema.producer]) {
|
|
20026
|
-
const filename2 =
|
|
20740
|
+
const filename2 = join7(directory2, "job-schema-" + randomUUID6() + ".sql");
|
|
20027
20741
|
writePrivate(filename2, schema);
|
|
20028
20742
|
try {
|
|
20029
20743
|
await jobCommand("backify_admin", state.adminPassword, "backify_jobs", ["-f", filename2]);
|
|
20030
20744
|
} finally {
|
|
20031
|
-
if (
|
|
20745
|
+
if (existsSync8(filename2)) unlinkSync3(filename2);
|
|
20032
20746
|
}
|
|
20033
20747
|
}
|
|
20034
20748
|
await adminSql("REVOKE ALL ON DATABASE backify_jobs FROM PUBLIC; GRANT CONNECT ON DATABASE backify_jobs TO backify_jobs; REVOKE CREATE ON SCHEMA public FROM PUBLIC; GRANT USAGE ON SCHEMA __backify_jobs TO backify_jobs; GRANT SELECT ON __backify_jobs.schema_version,__backify_jobs.producer_schema_version TO backify_jobs; GRANT SELECT,INSERT,UPDATE ON __backify_jobs.runs,__backify_jobs.attempts TO backify_jobs; GRANT SELECT,INSERT ON __backify_jobs.producers TO backify_jobs", "backify_jobs");
|
|
@@ -20057,7 +20771,7 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
20057
20771
|
...events ? [{ id: state.events.id, scope: state.scope, capability: "events", contractVersion: 1, logicalBinding: "default", provider: "postgres-events", resourceReference: "backify_events", ownership: "Managed", accessMode: "Gateway", permissions: ["events.emit", "events.receive", "events.complete", "events.inspect"], status: "ready" }] : [],
|
|
20058
20772
|
...jobs ? [{ id: state.jobs.id, scope: state.scope, capability: "jobs", contractVersion: 1, logicalBinding: "default", provider: "postgres-jobs", resourceReference: "backify_jobs", ownership: "Managed", accessMode: "Gateway", permissions: ["jobs.enqueue", "jobs.receive", "jobs.complete", "jobs.cancel", "jobs.inspect", "jobs.retry"], status: "ready" }] : []
|
|
20059
20773
|
];
|
|
20060
|
-
const snapshotPath =
|
|
20774
|
+
const snapshotPath = join7(directory2, "snapshot.json");
|
|
20061
20775
|
const writeSnapshot = () => writePrivate(snapshotPath, JSON.stringify({ schemaVersion: 1, configurationVersion: state.configurationVersion, expiresAt: new Date(Date.now() + 7 * 864e5).toISOString(), bindings }));
|
|
20062
20776
|
const renewConfiguration = () => {
|
|
20063
20777
|
if (stopping) throw new Error("Stopped local services cannot renew configuration");
|
|
@@ -20067,15 +20781,15 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
20067
20781
|
writeSnapshot();
|
|
20068
20782
|
};
|
|
20069
20783
|
writeSnapshot();
|
|
20070
|
-
const settingsPath =
|
|
20784
|
+
const settingsPath = join7(directory2, "appsettings.json");
|
|
20071
20785
|
writePrivate(settingsPath, JSON.stringify({ Hcr: {
|
|
20072
20786
|
Authentication: { PublicKeyPem: publicKey.export({ type: "spki", format: "pem" }), Issuer: "backify-local", Audience: "backify-local-hcr" },
|
|
20073
20787
|
SnapshotPath: snapshotPath,
|
|
20074
|
-
SnapshotJournalPath:
|
|
20788
|
+
SnapshotJournalPath: join7(directory2, "journal.json"),
|
|
20075
20789
|
Postgres: { Bindings: state.retainedDatabasePort ? { [state.databaseId]: { Scope: state.scope, ConnectionString: "Host=127.0.0.1;Port=" + state.retainedDatabasePort + ";Database=backify_local;Username=backify_local;Password=" + state.databasePassword } } : {} },
|
|
20076
20790
|
Events: { Bindings: state.retainedDatabasePort && state.events?.initialized ? { [state.events.id]: { Scope: state.scope, ConnectionString: "Host=127.0.0.1;Port=" + state.retainedDatabasePort + ";Database=backify_events;Username=backify_events;Password=" + state.events.password } } : {} },
|
|
20077
20791
|
Jobs: { Bindings: state.retainedDatabasePort && state.jobs?.initialized ? { [state.jobs.id]: { Scope: state.scope, ConnectionString: "Host=127.0.0.1;Port=" + state.retainedDatabasePort + ";Database=backify_jobs;Username=backify_jobs;Password=" + state.jobs.password, Artifacts: state.jobs.artifacts } } : {} },
|
|
20078
|
-
Storage: { Bindings: Object.fromEntries(Object.entries(state.storageBindings).map(([name2, identifier2]) => [identifier2, { Scope: state.scope, ResourceReference: name2, RootPath:
|
|
20792
|
+
Storage: { Bindings: Object.fromEntries(Object.entries(state.storageBindings).map(([name2, identifier2]) => [identifier2, { Scope: state.scope, ResourceReference: name2, RootPath: join7(directory2, "objects", identifier2) }])) }
|
|
20079
20793
|
} }));
|
|
20080
20794
|
issueToken();
|
|
20081
20795
|
notify("hcr", "starting");
|
|
@@ -20089,7 +20803,7 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
20089
20803
|
while (true) {
|
|
20090
20804
|
if (spawnFailed || hcr.exitCode !== null || hcr.signalCode !== null || Date.now() > deadline) throw new Error("Local HCR could not become ready; check the .NET runtime, bindings and retained journal");
|
|
20091
20805
|
try {
|
|
20092
|
-
const response = await fetch(runtimeUrl + "/v1/health", { headers: { authorization: "Bearer " +
|
|
20806
|
+
const response = await fetch(runtimeUrl + "/v1/health", { headers: { authorization: "Bearer " + readFileSync8(tokenPath, "utf8") }, signal: AbortSignal.timeout(500), redirect: "error", credentials: "omit", cache: "no-store" });
|
|
20093
20807
|
if (response.ok) {
|
|
20094
20808
|
const status = await response.json();
|
|
20095
20809
|
if (status.status === "ready" && status.configurationVersion === state.configurationVersion) break;
|
|
@@ -20098,8 +20812,8 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
20098
20812
|
}
|
|
20099
20813
|
await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
|
|
20100
20814
|
}
|
|
20101
|
-
if (events) await createHcrEventsProvider({ runtimeUrl, token: () =>
|
|
20102
|
-
if (jobs) await createHcrJobsProvider({ runtimeUrl, token: () =>
|
|
20815
|
+
if (events) await createHcrEventsProvider({ runtimeUrl, token: () => readFileSync8(tokenPath, "utf8") }).inspect();
|
|
20816
|
+
if (jobs) await createHcrJobsProvider({ runtimeUrl, token: () => readFileSync8(tokenPath, "utf8") }).inspect();
|
|
20103
20817
|
timer = setInterval(() => {
|
|
20104
20818
|
try {
|
|
20105
20819
|
issueToken();
|
|
@@ -20115,7 +20829,7 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
20115
20829
|
}
|
|
20116
20830
|
}, 36e5);
|
|
20117
20831
|
notify("hcr", "ready");
|
|
20118
|
-
const configurationFingerprint =
|
|
20832
|
+
const configurationFingerprint = createHash6("sha256").update(JSON.stringify({ mode: "full", capabilities: manifest.capabilities, migrations: migrations.map(({ name: name2, digest: digest4 }) => ({ name: name2, digest: digest4 })), ...jobs ? { jobArtifacts: state.jobs.artifacts } : {} })).digest("hex");
|
|
20119
20833
|
return { identityScope: { ...state.scope }, runtimeUrl, tokenPath, database, storage, events, jobs, databasePort, migrations: applied, configurationFingerprint, renewConfiguration, stop };
|
|
20120
20834
|
} catch (error) {
|
|
20121
20835
|
await stop();
|
|
@@ -20124,29 +20838,29 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
20124
20838
|
}
|
|
20125
20839
|
|
|
20126
20840
|
// src/local-job-auth.ts
|
|
20127
|
-
import { createHash as
|
|
20128
|
-
import { lstatSync as
|
|
20129
|
-
import { isAbsolute as
|
|
20841
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
20842
|
+
import { lstatSync as lstatSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
20843
|
+
import { isAbsolute as isAbsolute5, resolve as resolve8 } from "node:path";
|
|
20130
20844
|
function prepareLocalJobAuth(root, filename2) {
|
|
20131
20845
|
const read = (path, maximum) => {
|
|
20132
20846
|
assertLocalStatePath(root, path);
|
|
20133
|
-
const metadata =
|
|
20847
|
+
const metadata = lstatSync8(path);
|
|
20134
20848
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > maximum) throw new Error();
|
|
20135
|
-
const bytes =
|
|
20849
|
+
const bytes = readFileSync9(path);
|
|
20136
20850
|
if (bytes.length > maximum) throw new Error();
|
|
20137
20851
|
return bytes;
|
|
20138
20852
|
};
|
|
20139
20853
|
try {
|
|
20140
|
-
if (!filename2 ||
|
|
20141
|
-
const input = JSON.parse(new TextDecoder("utf8", { fatal: true }).decode(read(
|
|
20854
|
+
if (!filename2 || isAbsolute5(filename2)) throw new Error();
|
|
20855
|
+
const input = JSON.parse(new TextDecoder("utf8", { fatal: true }).decode(read(resolve8(root, filename2), 16384)));
|
|
20142
20856
|
if (!input || Object.keys(input).sort().join(",") !== "revision,schemaVersion,scope,stateFile,trustDigest,trustFile" || input.schemaVersion !== 1 || !Number.isSafeInteger(input.revision) || input.revision < 1 || typeof input.trustDigest !== "string" || input.trustDigest.length !== 64 || !/^[a-f0-9]{64}$/.test(input.trustDigest)) throw new Error();
|
|
20143
20857
|
if (!input.scope || Object.keys(input.scope).sort().join(",") !== "applicationId,environmentId,organizationId" || Object.values(input.scope).some((value) => typeof value !== "string" || value.length !== 36 || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value) || value === "00000000-0000-0000-0000-000000000000")) throw new Error();
|
|
20144
|
-
if ([input.trustFile, input.stateFile].some((path) => typeof path !== "string" || !path ||
|
|
20145
|
-
const trustFile =
|
|
20858
|
+
if ([input.trustFile, input.stateFile].some((path) => typeof path !== "string" || !path || isAbsolute5(path))) throw new Error();
|
|
20859
|
+
const trustFile = resolve8(root, input.trustFile), stateFile = resolve8(root, input.stateFile);
|
|
20146
20860
|
if (trustFile === stateFile) throw new Error();
|
|
20147
20861
|
const bytes = read(trustFile, 131072), trust = JSON.parse(new TextDecoder("utf8", { fatal: true }).decode(bytes));
|
|
20148
20862
|
read(stateFile, 2048);
|
|
20149
|
-
if (
|
|
20863
|
+
if (createHash7("sha256").update(bytes).digest("hex") !== input.trustDigest || trust.revision !== input.revision) throw new Error();
|
|
20150
20864
|
createPersistentApplicationAuthProvider(trust, { expectedScope: input.scope, stateFile });
|
|
20151
20865
|
return { trustFile, stateFile, scope: structuredClone(input.scope), revision: input.revision, trustDigest: input.trustDigest };
|
|
20152
20866
|
} catch {
|
|
@@ -20155,39 +20869,39 @@ function prepareLocalJobAuth(root, filename2) {
|
|
|
20155
20869
|
}
|
|
20156
20870
|
|
|
20157
20871
|
// src/local-job-trust-renewal.ts
|
|
20158
|
-
import { createHash as
|
|
20159
|
-
import { lstatSync as
|
|
20160
|
-
import { isAbsolute as
|
|
20872
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
20873
|
+
import { lstatSync as lstatSync10, readFileSync as readFileSync11 } from "node:fs";
|
|
20874
|
+
import { isAbsolute as isAbsolute7, relative as relative2, resolve as resolve10 } from "node:path";
|
|
20161
20875
|
|
|
20162
20876
|
// src/workload-trust-sync.ts
|
|
20163
|
-
import { lstatSync as
|
|
20164
|
-
import { isAbsolute as
|
|
20165
|
-
import { createHash as
|
|
20877
|
+
import { lstatSync as lstatSync9, readFileSync as readFileSync10 } from "node:fs";
|
|
20878
|
+
import { isAbsolute as isAbsolute6, resolve as resolve9 } from "node:path";
|
|
20879
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
20166
20880
|
function prepareWorkloadTrustSynchronization(filename2) {
|
|
20167
20881
|
try {
|
|
20168
|
-
if (!
|
|
20169
|
-
const metadata =
|
|
20882
|
+
if (!isAbsolute6(filename2)) throw new Error();
|
|
20883
|
+
const metadata = lstatSync9(filename2);
|
|
20170
20884
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > 65536) throw new Error();
|
|
20171
|
-
const bytes =
|
|
20885
|
+
const bytes = readFileSync10(filename2);
|
|
20172
20886
|
if (bytes.length > 65536) throw new Error();
|
|
20173
20887
|
const value = JSON.parse(new TextDecoder("utf8", { fatal: true }).decode(bytes));
|
|
20174
20888
|
const allowed = ["controlPlaneUrl", "agentId", "expectedScope", "managementIssuer", "managementAudience", "managementPublicKeyPem", "credentialFile", "trustFile", "stateFile", "intervalSeconds"];
|
|
20175
|
-
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((name2) => !allowed.includes(name2)) || typeof value.credentialFile !== "string" || !
|
|
20176
|
-
const credentialFile =
|
|
20889
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((name2) => !allowed.includes(name2)) || typeof value.credentialFile !== "string" || !isAbsolute6(value.credentialFile) || typeof value.trustFile !== "string" || !isAbsolute6(value.trustFile) || typeof value.stateFile !== "string" || !isAbsolute6(value.stateFile)) throw new Error();
|
|
20890
|
+
const credentialFile = resolve9(value.credentialFile), trustFile = resolve9(value.trustFile), stateFile = resolve9(value.stateFile);
|
|
20177
20891
|
const comparison = (filename3) => process.platform === "win32" ? filename3.toLowerCase() : filename3;
|
|
20178
20892
|
if ([trustFile, stateFile, stateFile + ".lock", stateFile + ".delivery.lock"].map(comparison).includes(comparison(credentialFile))) throw new Error();
|
|
20179
20893
|
const intervalSeconds = value.intervalSeconds ?? 30;
|
|
20180
20894
|
if (!Number.isSafeInteger(intervalSeconds) || intervalSeconds < 10 || intervalSeconds > 300) throw new Error();
|
|
20181
20895
|
const credential = () => {
|
|
20182
|
-
const metadata2 =
|
|
20896
|
+
const metadata2 = lstatSync9(credentialFile);
|
|
20183
20897
|
if (!metadata2.isFile() || metadata2.isSymbolicLink() || metadata2.nlink !== 1 || metadata2.size > 256 || process.platform !== "win32" && (metadata2.uid !== process.getuid?.() || (metadata2.mode & 63) !== 0)) throw new Error("Management credential unavailable");
|
|
20184
|
-
const bytes2 =
|
|
20898
|
+
const bytes2 = readFileSync10(credentialFile);
|
|
20185
20899
|
if (bytes2.length > 256) throw new Error("Management credential unavailable");
|
|
20186
20900
|
return new TextDecoder("utf8", { fatal: true }).decode(bytes2).trim();
|
|
20187
20901
|
};
|
|
20188
20902
|
const options = { controlPlaneUrl: value.controlPlaneUrl, agentId: value.agentId, expectedScope: value.expectedScope, managementIssuer: value.managementIssuer, managementAudience: value.managementAudience, managementPublicKeyPem: value.managementPublicKeyPem, credential, trustFile, stateFile };
|
|
20189
20903
|
const plan = planApplicationAuthSynchronization(options);
|
|
20190
|
-
return { options, plan, intervalSeconds, credentialFile, configurationDigest:
|
|
20904
|
+
return { options, plan, intervalSeconds, credentialFile, configurationDigest: createHash8("sha256").update(bytes).digest("hex") };
|
|
20191
20905
|
} catch {
|
|
20192
20906
|
throw new Error("Invalid operator trust-sync configuration");
|
|
20193
20907
|
}
|
|
@@ -20238,22 +20952,22 @@ async function workloadTrustSynchronization(args2, settings = {}) {
|
|
|
20238
20952
|
// src/local-job-trust-renewal.ts
|
|
20239
20953
|
function prepareLocalJobTrustRenewal(root, filename2, mount) {
|
|
20240
20954
|
try {
|
|
20241
|
-
if (!filename2 ||
|
|
20242
|
-
const configurationFile =
|
|
20955
|
+
if (!filename2 || isAbsolute7(filename2)) throw new Error();
|
|
20956
|
+
const configurationFile = resolve10(root, filename2);
|
|
20243
20957
|
assertLocalStatePath(root, configurationFile);
|
|
20244
20958
|
const configured = prepareWorkloadTrustSynchronization(configurationFile);
|
|
20245
20959
|
const { credential, ...options } = configured.options;
|
|
20246
20960
|
const same = (first, second) => process.platform === "win32" ? first.toLowerCase() === second.toLowerCase() : first === second;
|
|
20247
20961
|
if (!same(options.trustFile, mount.trustFile) || !same(options.stateFile, mount.stateFile) || ["organizationId", "applicationId", "environmentId"].some((name2) => options.expectedScope[name2] !== mount.scope[name2])) throw new Error();
|
|
20248
20962
|
assertLocalStatePath(root, configured.credentialFile);
|
|
20249
|
-
const metadata =
|
|
20963
|
+
const metadata = lstatSync10(configured.credentialFile);
|
|
20250
20964
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > 256 || process.platform !== "win32" && (metadata.uid !== process.getuid?.() || (metadata.mode & 63) !== 0)) throw new Error();
|
|
20251
20965
|
return { configurationFile, configurationDigest: configured.configurationDigest, credentialFile: configured.credentialFile, intervalSeconds: configured.intervalSeconds, options };
|
|
20252
20966
|
} catch {
|
|
20253
20967
|
throw new Error("Invalid reviewed native worker trust renewal; preserve trust and state and review explicitly");
|
|
20254
20968
|
}
|
|
20255
20969
|
}
|
|
20256
|
-
function createLocalJobTrustRenewal(root, mounts, settings = {}, dependencies = { createHash:
|
|
20970
|
+
function createLocalJobTrustRenewal(root, mounts, settings = {}, dependencies = { createHash: createHash9, lstatSync: lstatSync10, readFileSync: readFileSync11, isAbsolute: isAbsolute7, relative: relative2, resolve: resolve10, createPersistentApplicationAuthProvider, synchronizeApplicationAuthTrustWithDigest }) {
|
|
20257
20971
|
const { createHash: createHash16, lstatSync: lstatSync20, readFileSync: readFileSync25, isAbsolute: isAbsolute17, relative: relative11, resolve: resolve24, createPersistentApplicationAuthProvider: createPersistentApplicationAuthProvider2, synchronizeApplicationAuthTrustWithDigest: synchronizeApplicationAuthTrustWithDigest2 } = dependencies;
|
|
20258
20972
|
const key = (filename2) => process.platform === "win32" ? resolve24(filename2).toLowerCase() : resolve24(filename2);
|
|
20259
20973
|
const groups = [];
|
|
@@ -20352,34 +21066,34 @@ function applyLocalJobTrustRenewal(workers, updates, check) {
|
|
|
20352
21066
|
|
|
20353
21067
|
// src/local-job-executor.ts
|
|
20354
21068
|
function prepareLocalJobWorker(root, directory2, artifacts, manifest, authFile, trustSyncFile) {
|
|
20355
|
-
if (typeof directory2 !== "string" || !directory2 ||
|
|
21069
|
+
if (typeof directory2 !== "string" || !directory2 || isAbsolute8(directory2)) throw new Error("Native jobs require a repository-relative packaged worker directory");
|
|
20356
21070
|
const authenticationEnabled = !!manifest.capabilities.auth && manifest.capabilities.auth.enabled !== false;
|
|
20357
21071
|
if (authenticationEnabled && authFile === void 0) throw new Error("Native local packaged workers require separately reviewed application-auth trust mounts; local identity fixtures are not production issuer trust");
|
|
20358
21072
|
if (!authenticationEnabled && authFile !== void 0) throw new Error("Native worker trust mounts require an enabled auth capability");
|
|
20359
21073
|
if (trustSyncFile !== void 0 && authFile === void 0) throw new Error("Native worker trust renewal requires explicit reviewed auth mounts");
|
|
20360
21074
|
const authTrust = authFile === void 0 ? void 0 : prepareLocalJobAuth(root, authFile);
|
|
20361
21075
|
if (authTrust && trustSyncFile !== void 0) authTrust.renewal = prepareLocalJobTrustRenewal(root, trustSyncFile, authTrust);
|
|
20362
|
-
const workerFile =
|
|
21076
|
+
const workerFile = join8(resolve11(root, directory2), "workload.mjs"), metadataFile = join8(resolve11(root, directory2), "artifact.json");
|
|
20363
21077
|
for (const filename2 of [workerFile, metadataFile]) assertLocalStatePath(root, filename2);
|
|
20364
|
-
if (!
|
|
20365
|
-
const metadata = JSON.parse(
|
|
20366
|
-
const bundle =
|
|
20367
|
-
const actual =
|
|
21078
|
+
if (!lstatSync11(workerFile).isFile() || lstatSync11(workerFile).size > 16777216 || !lstatSync11(metadataFile).isFile() || lstatSync11(metadataFile).size > 65536) throw new Error("Invalid bounded native worker artifact");
|
|
21079
|
+
const metadata = JSON.parse(readFileSync12(metadataFile, "utf8"));
|
|
21080
|
+
const bundle = readFileSync12(workerFile);
|
|
21081
|
+
const actual = createHash10("sha256").update(bundle).digest("hex");
|
|
20368
21082
|
const scheduleDiscovery = bundle.subarray(0, 100).toString("utf8").startsWith('var __backifyWorkerProtocols = ["hcr-v1","hcr-schedules-v1"];\n');
|
|
20369
|
-
if (!metadata || metadata.schemaVersion !== 1 || metadata.kind !== "worker" || metadata.artifactVersion !== actual || metadata.manifestVersion !==
|
|
21083
|
+
if (!metadata || metadata.schemaVersion !== 1 || metadata.kind !== "worker" || metadata.artifactVersion !== actual || metadata.manifestVersion !== createHash10("sha256").update(JSON.stringify(manifest)).digest("hex")) throw new Error("Native worker kind, bundle or manifest pin does not match the reviewed artifact");
|
|
20370
21084
|
const artifact = validateLocalJobArtifacts(artifacts).find((item) => item.artifactVersion === actual);
|
|
20371
21085
|
if (!artifact) throw new Error("Native worker bundle is not an explicitly reviewed job artifact");
|
|
20372
21086
|
if (metadata.workerProtocols !== void 0 && (!scheduleDiscovery || JSON.stringify(metadata.workerProtocols) !== JSON.stringify(["hcr-v1", "hcr-schedules-v1"]))) throw new Error("Invalid worker protocol admission");
|
|
20373
21087
|
return { root, workerFile, artifact, schedulerEnabled: !!manifest.capabilities.scheduler && manifest.capabilities.scheduler.enabled !== false, ...scheduleDiscovery ? { scheduleDiscovery: true } : {}, ...authTrust ? { authTrust } : {} };
|
|
20374
21088
|
}
|
|
20375
21089
|
function readLocalJobArtifacts(root, filename2) {
|
|
20376
|
-
if (!filename2 ||
|
|
20377
|
-
const path =
|
|
21090
|
+
if (!filename2 || isAbsolute8(filename2)) throw new Error("Job artifact admission requires a repository-relative reviewed JSON file");
|
|
21091
|
+
const path = resolve11(root, filename2);
|
|
20378
21092
|
assertLocalStatePath(root, path);
|
|
20379
|
-
if (!
|
|
20380
|
-
return validateLocalJobArtifacts(JSON.parse(
|
|
21093
|
+
if (!lstatSync11(path).isFile() || lstatSync11(path).size > 16384) throw new Error("Reviewed job artifact admission exceeds supported bounds");
|
|
21094
|
+
return validateLocalJobArtifacts(JSON.parse(readFileSync12(path, "utf8")));
|
|
20381
21095
|
}
|
|
20382
|
-
function createLocalJobExecutor(options, dependencies = { spawn: spawn2, createHash:
|
|
21096
|
+
function createLocalJobExecutor(options, dependencies = { spawn: spawn2, createHash: createHash10, lstatSync: lstatSync11, readFileSync: readFileSync12, isAbsolute: isAbsolute8, relative: relative3, resolve: resolve11, validateWorkloadJobSchedules, createPersistentApplicationAuthProvider }) {
|
|
20383
21097
|
const { spawn: spawn6, createHash: createHash16, lstatSync: lstatSync20, readFileSync: readFileSync25, isAbsolute: isAbsolute17, relative: relative11, resolve: resolve24, validateWorkloadJobSchedules: validateWorkloadJobSchedules2, createPersistentApplicationAuthProvider: createPersistentApplicationAuthProvider2 } = dependencies;
|
|
20384
21098
|
const assertReady = () => {
|
|
20385
21099
|
for (let current = resolve24(options.workerFile); current !== options.root; current = resolve24(current, "..")) {
|
|
@@ -20512,9 +21226,9 @@ function createLocalJobExecutor(options, dependencies = { spawn: spawn2, createH
|
|
|
20512
21226
|
}
|
|
20513
21227
|
|
|
20514
21228
|
// src/local-native-jobs.ts
|
|
20515
|
-
import { createHash as
|
|
21229
|
+
import { createHash as createHash11 } from "node:crypto";
|
|
20516
21230
|
function createLocalNativeJobs(local, native, executor, artifact, scope, onFailure = () => {
|
|
20517
|
-
}, hash2 =
|
|
21231
|
+
}, hash2 = createHash11, catalog = { workloadJobSchedules, validateWorkloadJobSchedules, HcrJobsRequestError }, historical = []) {
|
|
20518
21232
|
const workers = [{ artifact, executor }, ...historical];
|
|
20519
21233
|
if (historical.length > 15 || new Set(workers.map((worker) => worker.artifact.artifactVersion)).size !== workers.length) throw new Error("Historical workers require bounded unique artifact pins");
|
|
20520
21234
|
const lanes = workers.flatMap((worker) => worker.artifact.jobs.map((policy) => ({ ...worker, policy })));
|
|
@@ -20772,9 +21486,9 @@ process.on("SIGTERM", stop); process.on("SIGINT", stop); process.once("disconnec
|
|
|
20772
21486
|
|
|
20773
21487
|
// src/local-frontend.ts
|
|
20774
21488
|
import { spawn as spawn3 } from "node:child_process";
|
|
20775
|
-
import { readFileSync as
|
|
21489
|
+
import { readFileSync as readFileSync14, realpathSync as realpathSync3, existsSync as existsSync9, statSync } from "node:fs";
|
|
20776
21490
|
import { createRequire as createRequire3 } from "node:module";
|
|
20777
|
-
import { dirname as
|
|
21491
|
+
import { dirname as dirname10, isAbsolute as isAbsolute10, join as join10, relative as relative5, resolve as resolve13 } from "node:path";
|
|
20778
21492
|
import { pathToFileURL } from "node:url";
|
|
20779
21493
|
|
|
20780
21494
|
// src/local-frontend-guard.ts
|
|
@@ -20841,24 +21555,24 @@ await server.listen();
|
|
|
20841
21555
|
`;
|
|
20842
21556
|
|
|
20843
21557
|
// src/local-static-frontend.ts
|
|
20844
|
-
import { createServer as
|
|
20845
|
-
import { lstatSync as
|
|
20846
|
-
import { extname as extname2, isAbsolute as
|
|
21558
|
+
import { createServer as createServer3 } from "node:http";
|
|
21559
|
+
import { lstatSync as lstatSync12, readFileSync as readFileSync13, realpathSync as realpathSync2, watch } from "node:fs";
|
|
21560
|
+
import { extname as extname2, isAbsolute as isAbsolute9, join as join9, relative as relative4, resolve as resolve12, sep as sep4 } from "node:path";
|
|
20847
21561
|
var contentTypes = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json", ".txt": "text/plain; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".ico": "image/x-icon", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf" };
|
|
20848
21562
|
var publicParts = (parts) => parts.every((part) => part.length > 0 && !part.startsWith(".") && !part.includes(":") && !part.includes("\\") && !part.includes("\0") && !["node_modules", "__backify"].includes(part.toLowerCase()));
|
|
20849
21563
|
function publicFile(directory2, pathname) {
|
|
20850
21564
|
const parts = pathname.split("/").slice(1);
|
|
20851
21565
|
if (parts.at(-1) === "") parts[parts.length - 1] = "index.html";
|
|
20852
21566
|
if (!publicParts(parts)) throw new Error("refused");
|
|
20853
|
-
const filename2 =
|
|
20854
|
-
if (part === ".." || part.startsWith(".." + sep4) ||
|
|
21567
|
+
const filename2 = resolve12(directory2, ...parts), part = relative4(directory2, filename2);
|
|
21568
|
+
if (part === ".." || part.startsWith(".." + sep4) || isAbsolute9(part) || !contentTypes[extname2(filename2).toLowerCase()]) throw new Error("refused");
|
|
20855
21569
|
let current = directory2;
|
|
20856
|
-
if (
|
|
21570
|
+
if (lstatSync12(current).isSymbolicLink() || realpathSync2(current) !== directory2) throw new Error("refused");
|
|
20857
21571
|
for (const segment of parts) {
|
|
20858
|
-
current =
|
|
20859
|
-
if (
|
|
21572
|
+
current = join9(current, segment);
|
|
21573
|
+
if (lstatSync12(current).isSymbolicLink()) throw new Error("refused");
|
|
20860
21574
|
}
|
|
20861
|
-
const stat =
|
|
21575
|
+
const stat = lstatSync12(filename2);
|
|
20862
21576
|
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 5242880) throw new Error("refused");
|
|
20863
21577
|
return filename2;
|
|
20864
21578
|
}
|
|
@@ -20873,7 +21587,7 @@ async function startLocalStaticFrontend(directory2, port, backendUrl) {
|
|
|
20873
21587
|
const watcher = watch(directory2, { recursive: true }, (_, filename2) => {
|
|
20874
21588
|
if (filename2 && publicParts(filename2.toString().split(/[\\/]/)) && contentTypes[extname2(filename2.toString()).toLowerCase()]) revision++;
|
|
20875
21589
|
});
|
|
20876
|
-
const server =
|
|
21590
|
+
const server = createServer3((request, response) => {
|
|
20877
21591
|
const send = (status, type, body) => {
|
|
20878
21592
|
response.writeHead(status, { "content-type": type, "cache-control": "no-store", "x-content-type-options": "nosniff", "content-length": Buffer.byteLength(body) });
|
|
20879
21593
|
response.end(request.method === "HEAD" ? void 0 : body);
|
|
@@ -20886,7 +21600,7 @@ async function startLocalStaticFrontend(directory2, port, backendUrl) {
|
|
|
20886
21600
|
if (pathname === "/__backify/revision") return send(200, "text/plain", String(revision));
|
|
20887
21601
|
if (pathname === "/__backify/config.json") return send(200, "application/json", JSON.stringify({ backendUrl: backendUrl ?? null }));
|
|
20888
21602
|
const filename2 = publicFile(directory2, pathname);
|
|
20889
|
-
const content =
|
|
21603
|
+
const content = readFileSync13(filename2);
|
|
20890
21604
|
if (content.length > 5242880) throw new Error("refused");
|
|
20891
21605
|
const extension = extname2(filename2).toLowerCase();
|
|
20892
21606
|
const body = extension === ".html" ? content.toString("utf8") + '\n<script src="/__backify/reload.js?revision=' + revision + '"></script>\n' : content;
|
|
@@ -20929,31 +21643,31 @@ function prepareLocalFrontend(root, options, privateFiles = []) {
|
|
|
20929
21643
|
if (options.frontendPort !== void 0) throw new Error("Frontend port requires an explicit frontend directory");
|
|
20930
21644
|
return void 0;
|
|
20931
21645
|
}
|
|
20932
|
-
const directory2 = realpathSync3(
|
|
21646
|
+
const directory2 = realpathSync3(resolve13(root, options.frontend));
|
|
20933
21647
|
const part = relative5(realpathSync3(root), directory2);
|
|
20934
|
-
if (part === ".." || part.startsWith("../") || part.startsWith("..\\") ||
|
|
21648
|
+
if (part === ".." || part.startsWith("../") || part.startsWith("..\\") || isAbsolute10(part) || !statSync(directory2).isDirectory()) throw new Error("Frontend must be a directory inside this repository");
|
|
20935
21649
|
if (options.frontendAdapter === "static") {
|
|
20936
21650
|
if (part && part.split(/[\\/]/).some((segment) => segment.startsWith(".") || segment.toLowerCase() === "node_modules")) throw new Error("Static frontend must not use a private state or dependency directory");
|
|
20937
21651
|
return prepareLocalStaticFrontend(directory2);
|
|
20938
21652
|
}
|
|
20939
|
-
if (!
|
|
20940
|
-
const require3 = createRequire3(
|
|
21653
|
+
if (!existsSync9(join10(directory2, "package.json"))) throw new Error("Frontend requires an existing package.json and installed Vite");
|
|
21654
|
+
const require3 = createRequire3(join10(directory2, "package.json"));
|
|
20941
21655
|
let packagePath;
|
|
20942
21656
|
try {
|
|
20943
21657
|
packagePath = require3.resolve("vite/package.json");
|
|
20944
21658
|
} catch {
|
|
20945
21659
|
throw new Error("Frontend requires locally installed Vite; Backify does not install dependencies automatically");
|
|
20946
21660
|
}
|
|
20947
|
-
const metadata = JSON.parse(
|
|
21661
|
+
const metadata = JSON.parse(readFileSync14(packagePath, "utf8"));
|
|
20948
21662
|
if (metadata.name !== "vite" || typeof metadata.bin?.vite !== "string") throw new Error("Installed Vite has no supported CLI entrypoint");
|
|
20949
|
-
const executable = realpathSync3(
|
|
20950
|
-
const executablePart = relative5(realpathSync3(
|
|
20951
|
-
if (executablePart === ".." || executablePart.startsWith("../") || executablePart.startsWith("..\\") ||
|
|
21663
|
+
const executable = realpathSync3(resolve13(dirname10(packagePath), metadata.bin.vite));
|
|
21664
|
+
const executablePart = relative5(realpathSync3(dirname10(packagePath)), executable);
|
|
21665
|
+
if (executablePart === ".." || executablePart.startsWith("../") || executablePart.startsWith("..\\") || isAbsolute10(executablePart)) throw new Error("Vite CLI entrypoint must remain inside its package");
|
|
20952
21666
|
const timeout = options.frontendStartupTimeoutMs ?? 3e4;
|
|
20953
21667
|
if (!Number.isInteger(timeout) || timeout < 100 || timeout > 12e4) throw new Error("Frontend startup timeout must be 100-120000 milliseconds");
|
|
20954
21668
|
const modulePath = realpathSync3(require3.resolve("vite"));
|
|
20955
|
-
const modulePart = relative5(realpathSync3(
|
|
20956
|
-
if (modulePart === ".." || modulePart.startsWith("../") || modulePart.startsWith("..\\") ||
|
|
21669
|
+
const modulePart = relative5(realpathSync3(dirname10(packagePath)), modulePath);
|
|
21670
|
+
if (modulePart === ".." || modulePart.startsWith("../") || modulePart.startsWith("..\\") || isAbsolute10(modulePart)) throw new Error("Vite module must remain inside its package");
|
|
20957
21671
|
return { adapter: "vite", directory: directory2, modulePath, timeout, privateFiles };
|
|
20958
21672
|
}
|
|
20959
21673
|
async function stopFrontend(child) {
|
|
@@ -21008,17 +21722,19 @@ async function startLocalFrontend(prepared, port, backendUrl) {
|
|
|
21008
21722
|
}
|
|
21009
21723
|
|
|
21010
21724
|
// src/local-identity.ts
|
|
21011
|
-
import { readFileSync as
|
|
21012
|
-
import { resolve as
|
|
21725
|
+
import { readFileSync as readFileSync15, statSync as statSync2 } from "node:fs";
|
|
21726
|
+
import { resolve as resolve14 } from "node:path";
|
|
21013
21727
|
|
|
21014
21728
|
// ../sdk/dist/local-identity.js
|
|
21015
|
-
import { createHash as
|
|
21729
|
+
import { createHash as createHash12, randomBytes as randomBytes4 } from "node:crypto";
|
|
21016
21730
|
function createLocalIdentityProvider(options) {
|
|
21017
21731
|
const serverOnly = () => {
|
|
21018
21732
|
if (typeof window !== "undefined")
|
|
21019
21733
|
throw new Error("Local development identities are server-only");
|
|
21020
21734
|
};
|
|
21021
21735
|
serverOnly();
|
|
21736
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production")
|
|
21737
|
+
throw new Error("Local Backify identities must not run in production");
|
|
21022
21738
|
const uuid5 = (value) => typeof value === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value) && value !== "00000000-0000-0000-0000-000000000000";
|
|
21023
21739
|
const text3 = (value, maximum) => typeof value === "string" && value.length > 0 && value.length <= maximum && !/[\x00-\x1f\x7f]/.test(value);
|
|
21024
21740
|
if (!options.scope || Object.keys(options.scope).length !== 3 || !uuid5(options.scope.organizationId) || !uuid5(options.scope.applicationId) || !uuid5(options.scope.environmentId) || !text3(options.organizationName, 200) || !Array.isArray(options.users) || options.users.length < 1 || options.users.length > 32 || !Array.isArray(options.requestOrigins) || options.requestOrigins.length < 1 || options.requestOrigins.length > 8)
|
|
@@ -21026,9 +21742,11 @@ function createLocalIdentityProvider(options) {
|
|
|
21026
21742
|
const scope = { ...options.scope }, organizationName = options.organizationName;
|
|
21027
21743
|
const users = /* @__PURE__ */ new Map();
|
|
21028
21744
|
for (const user of options.users) {
|
|
21029
|
-
if (!user || Object.keys(user).some((name2) => !["id", "displayName", "email"].includes(name2)) || !uuid5(user.id) || users.has(user.id) || !text3(user.displayName, 200) || user.email !== void 0 && !text3(user.email, 320))
|
|
21745
|
+
if (!user || Object.keys(user).some((name2) => !["id", "displayName", "email", "roles"].includes(name2)) || !uuid5(user.id) || users.has(user.id) || !text3(user.displayName, 200) || user.email !== void 0 && !text3(user.email, 320))
|
|
21030
21746
|
throw new Error("Invalid local identity user");
|
|
21031
|
-
|
|
21747
|
+
if (user.roles !== void 0 && (!Array.isArray(user.roles) || user.roles.length > 32 || user.roles.some((role) => !role || Object.keys(role).some((name2) => !["name", "permissions"].includes(name2)) || !text3(role.name, 100) || !Array.isArray(role.permissions) || role.permissions.length > 128 || role.permissions.some((permission) => !text3(permission, 100)) || new Set(role.permissions).size !== role.permissions.length) || new Set(user.roles.map((role) => role.name)).size !== user.roles.length))
|
|
21748
|
+
throw new Error("Invalid local identity roles");
|
|
21749
|
+
users.set(user.id, structuredClone(user));
|
|
21032
21750
|
}
|
|
21033
21751
|
const origins = /* @__PURE__ */ new Set();
|
|
21034
21752
|
for (const value of options.requestOrigins) {
|
|
@@ -21051,7 +21769,7 @@ function createLocalIdentityProvider(options) {
|
|
|
21051
21769
|
return value;
|
|
21052
21770
|
};
|
|
21053
21771
|
const sessions = /* @__PURE__ */ new Map();
|
|
21054
|
-
const digest4 = (token) =>
|
|
21772
|
+
const digest4 = (token) => createHash12("sha256").update(token).digest("hex");
|
|
21055
21773
|
const tokenPattern = /^backify-local-user_[a-zA-Z0-9_-]{43}$/;
|
|
21056
21774
|
const requestAuth = {
|
|
21057
21775
|
forRequest(request) {
|
|
@@ -21068,7 +21786,7 @@ function createLocalIdentityProvider(options) {
|
|
|
21068
21786
|
if (!session || session.issued > time || session.expires <= time)
|
|
21069
21787
|
throw new ApplicationAuthenticationError("application_identity_invalid");
|
|
21070
21788
|
const user = users.get(session.userId);
|
|
21071
|
-
return { ...user, roles: [], claims: { ...scope, localDevelopment: true } };
|
|
21789
|
+
return { ...structuredClone(user), roles: structuredClone(user.roles ?? []), claims: { ...scope, localDevelopment: true } };
|
|
21072
21790
|
};
|
|
21073
21791
|
return { currentUser: async () => identity(), currentOrganization: async () => identity() ? { id: scope.organizationId, name: organizationName } : null };
|
|
21074
21792
|
}
|
|
@@ -21077,7 +21795,7 @@ function createLocalIdentityProvider(options) {
|
|
|
21077
21795
|
requestAuth,
|
|
21078
21796
|
profiles() {
|
|
21079
21797
|
serverOnly();
|
|
21080
|
-
return [...users.values()].map((user) => (
|
|
21798
|
+
return [...users.values()].map((user) => structuredClone(user));
|
|
21081
21799
|
},
|
|
21082
21800
|
issue(userId, lifetimeSeconds = 300) {
|
|
21083
21801
|
const time = now();
|
|
@@ -21088,7 +21806,7 @@ function createLocalIdentityProvider(options) {
|
|
|
21088
21806
|
sessions.delete(hash2);
|
|
21089
21807
|
if (sessions.size >= 128)
|
|
21090
21808
|
throw new ApplicationAuthenticationError("application_identity_unavailable");
|
|
21091
|
-
const accessToken = "backify-local-user_" +
|
|
21809
|
+
const accessToken = "backify-local-user_" + randomBytes4(32).toString("base64url"), expires = time + lifetimeSeconds;
|
|
21092
21810
|
sessions.set(digest4(accessToken), { userId, issued: time, expires });
|
|
21093
21811
|
return { accessToken, tokenType: "Bearer", expiresAt: new Date(expires * 1e3).toISOString() };
|
|
21094
21812
|
},
|
|
@@ -21104,22 +21822,22 @@ function createLocalIdentityProvider(options) {
|
|
|
21104
21822
|
}
|
|
21105
21823
|
|
|
21106
21824
|
// src/local-identity.ts
|
|
21107
|
-
import { randomUUID as
|
|
21825
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
21108
21826
|
function prepareLocalIdentity(root, filename2) {
|
|
21109
21827
|
if (filename2 === void 0) return void 0;
|
|
21110
|
-
const path =
|
|
21828
|
+
const path = resolve14(root, filename2);
|
|
21111
21829
|
assertLocalStatePath(root, path);
|
|
21112
21830
|
if (!statSync2(path).isFile() || statSync2(path).size > 16384) throw new Error("Local identity profiles require a file at most 16 KiB");
|
|
21113
21831
|
let input;
|
|
21114
21832
|
try {
|
|
21115
|
-
input = JSON.parse(
|
|
21833
|
+
input = JSON.parse(readFileSync15(path, "utf8"));
|
|
21116
21834
|
} catch {
|
|
21117
21835
|
throw new Error("Invalid local identity profile JSON");
|
|
21118
21836
|
}
|
|
21119
21837
|
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((name2) => !["version", "organizationName", "users"].includes(name2))) throw new Error("Invalid local identity profiles");
|
|
21120
21838
|
const value = input;
|
|
21121
21839
|
if (value.version !== 1) throw new Error("Invalid local identity profile version");
|
|
21122
|
-
const identity = createLocalIdentityProvider({ scope: { organizationId:
|
|
21840
|
+
const identity = createLocalIdentityProvider({ scope: { organizationId: randomUUID7(), applicationId: randomUUID7(), environmentId: randomUUID7() }, organizationName: value.organizationName, users: value.users, requestOrigins: ["http://127.0.0.1:3000"] });
|
|
21123
21841
|
return { organizationName: value.organizationName, users: identity.profiles() };
|
|
21124
21842
|
}
|
|
21125
21843
|
function localIdentityCommand(args2) {
|
|
@@ -21145,17 +21863,17 @@ function localIdentityCommand(args2) {
|
|
|
21145
21863
|
}
|
|
21146
21864
|
|
|
21147
21865
|
// src/local-job-history.ts
|
|
21148
|
-
import { lstatSync as
|
|
21149
|
-
import { isAbsolute as
|
|
21866
|
+
import { lstatSync as lstatSync13, readFileSync as readFileSync16 } from "node:fs";
|
|
21867
|
+
import { isAbsolute as isAbsolute11, resolve as resolve15 } from "node:path";
|
|
21150
21868
|
function prepareLocalJobHistory(root, filename2, artifacts, manifest, current) {
|
|
21151
21869
|
if (filename2 === void 0) return [];
|
|
21152
21870
|
const read = (filename3, maximum) => {
|
|
21153
|
-
if (typeof filename3 !== "string" || !filename3 ||
|
|
21154
|
-
const path =
|
|
21871
|
+
if (typeof filename3 !== "string" || !filename3 || isAbsolute11(filename3)) throw new Error();
|
|
21872
|
+
const path = resolve15(root, filename3);
|
|
21155
21873
|
assertLocalStatePath(root, path);
|
|
21156
|
-
const metadata =
|
|
21874
|
+
const metadata = lstatSync13(path);
|
|
21157
21875
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > maximum) throw new Error();
|
|
21158
|
-
const bytes =
|
|
21876
|
+
const bytes = readFileSync16(path);
|
|
21159
21877
|
if (bytes.length > maximum) throw new Error();
|
|
21160
21878
|
return new TextDecoder("utf8", { fatal: true }).decode(bytes);
|
|
21161
21879
|
};
|
|
@@ -21165,7 +21883,7 @@ function prepareLocalJobHistory(root, filename2, artifacts, manifest, current) {
|
|
|
21165
21883
|
if (!input || Object.keys(input).sort().join(",") !== "schemaVersion,workers" || input.schemaVersion !== 1 || !Array.isArray(input.workers) || input.workers.length < 1 || input.workers.length > 15) throw new Error();
|
|
21166
21884
|
const versions = /* @__PURE__ */ new Set([current.artifact.artifactVersion]);
|
|
21167
21885
|
for (const item of input.workers) {
|
|
21168
|
-
if (!item || Object.keys(item).some((name2) => !["artifactVersion", "authFile", "trustSyncFile", "manifestFile", "workerDirectory"].includes(name2)) || ["artifactVersion", "manifestFile", "workerDirectory"].some((name2) => !Object.hasOwn(item, name2)) || typeof item.artifactVersion !== "string" || item.artifactVersion.length !== 64 || !/^[a-f0-9]{64}$/.test(item.artifactVersion) || versions.has(item.artifactVersion) || !artifacts.some((artifact) => artifact.artifactVersion === item.artifactVersion) || typeof item.workerDirectory !== "string" || !item.workerDirectory ||
|
|
21886
|
+
if (!item || Object.keys(item).some((name2) => !["artifactVersion", "authFile", "trustSyncFile", "manifestFile", "workerDirectory"].includes(name2)) || ["artifactVersion", "manifestFile", "workerDirectory"].some((name2) => !Object.hasOwn(item, name2)) || typeof item.artifactVersion !== "string" || item.artifactVersion.length !== 64 || !/^[a-f0-9]{64}$/.test(item.artifactVersion) || versions.has(item.artifactVersion) || !artifacts.some((artifact) => artifact.artifactVersion === item.artifactVersion) || typeof item.workerDirectory !== "string" || !item.workerDirectory || isAbsolute11(item.workerDirectory)) throw new Error();
|
|
21169
21887
|
versions.add(item.artifactVersion);
|
|
21170
21888
|
}
|
|
21171
21889
|
return input.workers.map((item) => {
|
|
@@ -21201,30 +21919,30 @@ function prepareLocalJobHistory(root, filename2, artifacts, manifest, current) {
|
|
|
21201
21919
|
}
|
|
21202
21920
|
|
|
21203
21921
|
// src/local-secrets.ts
|
|
21204
|
-
import { createCipheriv, createDecipheriv, randomBytes as
|
|
21205
|
-
import { closeSync as closeSync3, existsSync as
|
|
21206
|
-
import { dirname as
|
|
21922
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes5, randomUUID as randomUUID8 } from "node:crypto";
|
|
21923
|
+
import { closeSync as closeSync3, existsSync as existsSync10, fsyncSync as fsyncSync3, lstatSync as lstatSync14, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync17, realpathSync as realpathSync4, writeFileSync as writeFileSync9 } from "node:fs";
|
|
21924
|
+
import { dirname as dirname11, isAbsolute as isAbsolute12, join as join11, relative as relative6, resolve as resolve16 } from "node:path";
|
|
21207
21925
|
var maximumBytes2 = 1048576;
|
|
21208
21926
|
var secretName = /^[A-Z][A-Z0-9_]{0,127}$/;
|
|
21209
21927
|
var failure = () => new Error("Local secret vault unavailable or invalid; review the vault and external key privately");
|
|
21210
21928
|
function vaultPath(root) {
|
|
21211
|
-
return
|
|
21929
|
+
return join11(realpathSync4(root), ".backify", "dev", "secrets.enc.json");
|
|
21212
21930
|
}
|
|
21213
21931
|
function inside(root, filename2) {
|
|
21214
21932
|
const part = relative6(root, filename2);
|
|
21215
|
-
return part !== ".." && !part.startsWith("../") && !part.startsWith("..\\") && !
|
|
21933
|
+
return part !== ".." && !part.startsWith("../") && !part.startsWith("..\\") && !isAbsolute12(part);
|
|
21216
21934
|
}
|
|
21217
21935
|
function privateFile(filename2, limit) {
|
|
21218
|
-
for (let current =
|
|
21219
|
-
if (
|
|
21220
|
-
if (
|
|
21936
|
+
for (let current = resolve16(filename2); ; current = dirname11(current)) {
|
|
21937
|
+
if (lstatSync14(current).isSymbolicLink()) throw failure();
|
|
21938
|
+
if (dirname11(current) === current) break;
|
|
21221
21939
|
}
|
|
21222
|
-
const stat =
|
|
21940
|
+
const stat = lstatSync14(filename2);
|
|
21223
21941
|
if (!stat.isFile() || stat.size > limit || stat.nlink !== 1 || process.platform !== "win32" && (stat.mode & 63) !== 0) throw failure();
|
|
21224
|
-
return
|
|
21942
|
+
return readFileSync17(filename2);
|
|
21225
21943
|
}
|
|
21226
21944
|
function externalKey(root, filename2) {
|
|
21227
|
-
const keyFile =
|
|
21945
|
+
const keyFile = resolve16(filename2);
|
|
21228
21946
|
if (inside(realpathSync4(root), keyFile) || inside(realpathSync4(root), realpathSync4(keyFile))) throw failure();
|
|
21229
21947
|
const encoded = privateFile(keyFile, 64).toString("utf8");
|
|
21230
21948
|
if (!/^[a-f0-9]{64}$/.test(encoded)) throw failure();
|
|
@@ -21238,7 +21956,7 @@ function context(root, application) {
|
|
|
21238
21956
|
}
|
|
21239
21957
|
function encrypt(root, application, key, secrets) {
|
|
21240
21958
|
validateSecrets(secrets);
|
|
21241
|
-
const nonce =
|
|
21959
|
+
const nonce = randomBytes5(12), cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
21242
21960
|
cipher.setAAD(context(root, application));
|
|
21243
21961
|
const plaintext = Buffer.from(JSON.stringify(secrets));
|
|
21244
21962
|
try {
|
|
@@ -21253,22 +21971,22 @@ function encrypt(root, application, key, secrets) {
|
|
|
21253
21971
|
function writePrivate2(filename2, value) {
|
|
21254
21972
|
const descriptor2 = openSync3(filename2, "wx", 384);
|
|
21255
21973
|
try {
|
|
21256
|
-
|
|
21974
|
+
writeFileSync9(descriptor2, value);
|
|
21257
21975
|
fsyncSync3(descriptor2);
|
|
21258
21976
|
} finally {
|
|
21259
21977
|
closeSync3(descriptor2);
|
|
21260
21978
|
}
|
|
21261
21979
|
}
|
|
21262
21980
|
function ensureIgnored(root) {
|
|
21263
|
-
const filename2 =
|
|
21981
|
+
const filename2 = join11(root, ".gitignore");
|
|
21264
21982
|
assertLocalStatePath(root, filename2);
|
|
21265
|
-
const current =
|
|
21266
|
-
if (!current.split(/\r?\n/).some((line) => line.trim() === ".backify/"))
|
|
21983
|
+
const current = existsSync10(filename2) ? readFileSync17(filename2, "utf8") : "";
|
|
21984
|
+
if (!current.split(/\r?\n/).some((line) => line.trim() === ".backify/")) writeFileSync9(filename2, current + (current && !current.endsWith("\n") ? "\n" : "") + ".backify/\n");
|
|
21267
21985
|
}
|
|
21268
21986
|
function readLocalSecrets(root, application, keyFile) {
|
|
21269
21987
|
const filename2 = vaultPath(root);
|
|
21270
21988
|
assertLocalStatePath(root, filename2);
|
|
21271
|
-
if (!
|
|
21989
|
+
if (!existsSync10(filename2) && keyFile === void 0) return {};
|
|
21272
21990
|
if (!keyFile) throw new Error("Encrypted local secrets require --local-secrets-key <external-key-file>");
|
|
21273
21991
|
let key, plaintext;
|
|
21274
21992
|
try {
|
|
@@ -21296,22 +22014,22 @@ function readLocalSecrets(root, application, keyFile) {
|
|
|
21296
22014
|
}
|
|
21297
22015
|
}
|
|
21298
22016
|
function initializeLocalSecrets(root, application, keyFile) {
|
|
21299
|
-
const filename2 = vaultPath(root), external =
|
|
22017
|
+
const filename2 = vaultPath(root), external = resolve16(keyFile);
|
|
21300
22018
|
assertLocalStatePath(root, filename2);
|
|
21301
|
-
if (
|
|
21302
|
-
if (inside(realpathSync4(root), external) || inside(realpathSync4(root), realpathSync4(
|
|
21303
|
-
for (let current =
|
|
21304
|
-
const stat =
|
|
22019
|
+
if (existsSync10(filename2) || existsSync10(external)) throw new Error("Local secret initialization never overwrites an existing vault or key");
|
|
22020
|
+
if (inside(realpathSync4(root), external) || inside(realpathSync4(root), realpathSync4(dirname11(external)))) throw new Error("Local secret key must be outside the repository in an existing private directory");
|
|
22021
|
+
for (let current = dirname11(external); ; current = dirname11(current)) {
|
|
22022
|
+
const stat = lstatSync14(current);
|
|
21305
22023
|
if (stat.isSymbolicLink()) throw failure();
|
|
21306
|
-
if (current ===
|
|
21307
|
-
if (
|
|
22024
|
+
if (current === dirname11(external) && process.platform !== "win32" && (stat.mode & 63) !== 0) throw failure();
|
|
22025
|
+
if (dirname11(current) === current) break;
|
|
21308
22026
|
}
|
|
21309
22027
|
ensureIgnored(root);
|
|
21310
22028
|
const release2 = acquireLocalLease(root, ".backify/dev/secrets.lock");
|
|
21311
|
-
const key =
|
|
22029
|
+
const key = randomBytes5(32);
|
|
21312
22030
|
try {
|
|
21313
|
-
if (
|
|
21314
|
-
|
|
22031
|
+
if (existsSync10(filename2)) throw failure();
|
|
22032
|
+
mkdirSync5(dirname11(filename2), { recursive: true, mode: 448 });
|
|
21315
22033
|
writePrivate2(external, key.toString("hex"));
|
|
21316
22034
|
writePrivate2(filename2, encrypt(root, application, key, {}));
|
|
21317
22035
|
} finally {
|
|
@@ -21330,7 +22048,7 @@ function setLocalSecret(root, application, keyFile, name2, value) {
|
|
|
21330
22048
|
const secrets = readLocalSecrets(root, application, keyFile);
|
|
21331
22049
|
secrets[name2] = value;
|
|
21332
22050
|
key = externalKey(root, keyFile);
|
|
21333
|
-
const temporary = filename2 + "." +
|
|
22051
|
+
const temporary = filename2 + "." + randomUUID8() + ".tmp";
|
|
21334
22052
|
assertLocalStatePath(root, temporary);
|
|
21335
22053
|
writePrivate2(temporary, encrypt(root, application, key, secrets));
|
|
21336
22054
|
renameLocalFile(temporary, filename2);
|
|
@@ -21367,27 +22085,27 @@ async function localSecretsCommand(args2, root, application, input = process.std
|
|
|
21367
22085
|
|
|
21368
22086
|
// src/local-development.ts
|
|
21369
22087
|
function localManifest(root = process.cwd()) {
|
|
21370
|
-
const filename2 =
|
|
21371
|
-
if (!
|
|
21372
|
-
const manifest = parseApplicationManifest(
|
|
21373
|
-
const entry =
|
|
21374
|
-
if (!
|
|
22088
|
+
const filename2 = join12(root, "backify.yaml");
|
|
22089
|
+
if (!existsSync11(filename2)) throw new Error("Create backify.yaml with version, application, runtime and explicit capabilities before running backify dev");
|
|
22090
|
+
const manifest = parseApplicationManifest(readFileSync18(filename2, "utf8"));
|
|
22091
|
+
const entry = resolve17(root, manifest.runtime.entrypoint);
|
|
22092
|
+
if (!existsSync11(entry) || !inside2(root, realpathSync5(entry))) throw new Error("Runtime entrypoint must exist inside this repository");
|
|
21375
22093
|
const discovered = analyzeSources(repositorySources(root));
|
|
21376
|
-
const diagnostics = manifestDiagnostics(manifest, Object.keys(discovered.capabilities));
|
|
22094
|
+
const diagnostics = manifestDiagnostics(manifest, Object.keys(discovered.capabilities), discovered.capabilities.auth?.configuration);
|
|
21377
22095
|
if (diagnostics.length) throw new Error(diagnostics.map((diagnostic) => diagnostic.message).join("; "));
|
|
21378
22096
|
return manifest;
|
|
21379
22097
|
}
|
|
21380
22098
|
function inside2(root, path) {
|
|
21381
22099
|
const part = relative7(realpathSync5(root), path);
|
|
21382
|
-
return part !== ".." && !part.startsWith(".." + (process.platform === "win32" ? "\\" : "/")) && !
|
|
22100
|
+
return part !== ".." && !part.startsWith(".." + (process.platform === "win32" ? "\\" : "/")) && !isAbsolute13(part);
|
|
21383
22101
|
}
|
|
21384
22102
|
function sessionPath(root) {
|
|
21385
|
-
return
|
|
22103
|
+
return join12(root, ".backify", "dev", "session.json");
|
|
21386
22104
|
}
|
|
21387
22105
|
async function localRequest(path, input, root = process.cwd()) {
|
|
21388
22106
|
if (!/^\/v1\/[a-z/-]+$/.test(path)) throw new Error("Invalid local command path");
|
|
21389
|
-
if (!
|
|
21390
|
-
const session = JSON.parse(
|
|
22107
|
+
if (!existsSync11(sessionPath(root))) throw new Error("Start backify dev before using local commands");
|
|
22108
|
+
const session = JSON.parse(readFileSync18(sessionPath(root), "utf8"));
|
|
21391
22109
|
if (session.version !== 1 || !Number.isInteger(session.port) || session.port < 1 || session.port > 65535 || !/^[a-f0-9]{64}$/.test(session.token)) throw new Error("Invalid local session");
|
|
21392
22110
|
const response = await fetch("http://127.0.0.1:" + session.port + path, { method: input === void 0 ? "GET" : "POST", headers: { authorization: "Bearer " + session.token, "content-type": "application/json" }, body: input === void 0 ? void 0 : JSON.stringify(input), redirect: "error", signal: AbortSignal.timeout(35e3) });
|
|
21393
22111
|
if (!response.ok) {
|
|
@@ -21420,7 +22138,7 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21420
22138
|
if (options.jobTrustSyncFile !== void 0 && options.jobAuthFile === void 0) throw new Error("Native worker trust renewal requires explicit reviewed auth mounts");
|
|
21421
22139
|
if (options.jobHistoryFile !== void 0 && options.jobWorkerDirectory === void 0) throw new Error("Historical workers require paired reviewed job artifacts and current worker");
|
|
21422
22140
|
if (options.jobAuthFile !== void 0 && options.identityUsers !== void 0) throw new Error("Reviewed worker issuer trust cannot be mixed with local identity fixtures");
|
|
21423
|
-
const root =
|
|
22141
|
+
const root = resolve17(options.root ?? process.cwd());
|
|
21424
22142
|
const manifest = localManifest(root);
|
|
21425
22143
|
const validateSecretIntent = (values, intent = manifest) => {
|
|
21426
22144
|
if (Object.keys(values).length && (!intent.capabilities.secrets || intent.capabilities.secrets.enabled === false)) throw new Error("Encrypted local secrets require enabled capabilities.secrets");
|
|
@@ -21437,18 +22155,18 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21437
22155
|
createLocalJobTrustRenewal(root, authMounts);
|
|
21438
22156
|
const identityProfiles = prepareLocalIdentity(root, options.identityUsers);
|
|
21439
22157
|
if (identityProfiles && (!manifest.capabilities.auth || manifest.capabilities.auth.enabled === false || options.backendPort === void 0)) throw new Error("Explicit local identities require capabilities.auth and --backend-port");
|
|
21440
|
-
const preparedFrontend = prepareLocalFrontend(root, options, options.localSecretsKey ? [realpathSync5(
|
|
21441
|
-
const ignorePath =
|
|
21442
|
-
if (
|
|
21443
|
-
const ignore =
|
|
21444
|
-
if (!ignore.split(/\r?\n/).some((line) => line.trim() === ".backify/"))
|
|
22158
|
+
const preparedFrontend = prepareLocalFrontend(root, options, options.localSecretsKey ? [realpathSync5(resolve17(options.localSecretsKey))] : []);
|
|
22159
|
+
const ignorePath = join12(root, ".gitignore");
|
|
22160
|
+
if (existsSync11(ignorePath) && lstatSync15(ignorePath).isSymbolicLink()) throw new Error("Local development cannot update a symlinked .gitignore");
|
|
22161
|
+
const ignore = existsSync11(ignorePath) ? readFileSync18(ignorePath, "utf8") : "";
|
|
22162
|
+
if (!ignore.split(/\r?\n/).some((line) => line.trim() === ".backify/")) writeFileSync10(ignorePath, ignore + (ignore.endsWith("\n") || !ignore ? "" : "\n") + ".backify/\n");
|
|
21445
22163
|
if (options.port !== void 0 && (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)) throw new Error("Dashboard port must be 0-65535");
|
|
21446
|
-
const directory2 =
|
|
22164
|
+
const directory2 = join12(root, ".backify", "dev");
|
|
21447
22165
|
assertLocalStatePath(root, directory2);
|
|
21448
|
-
for (const filename2 of ["session.json", "session.json.tmp", "worker.mjs", "worker.mjs.map", "state.json", "state.json.tmp"]) assertLocalStatePath(root,
|
|
21449
|
-
|
|
22166
|
+
for (const filename2 of ["session.json", "session.json.tmp", "worker.mjs", "worker.mjs.map", "state.json", "state.json.tmp"]) assertLocalStatePath(root, join12(directory2, filename2));
|
|
22167
|
+
mkdirSync6(directory2, { recursive: true });
|
|
21450
22168
|
if (!inside2(root, realpathSync5(directory2))) throw new Error("Local state directory must remain inside this repository");
|
|
21451
|
-
if (
|
|
22169
|
+
if (existsSync11(sessionPath(root))) {
|
|
21452
22170
|
try {
|
|
21453
22171
|
await localRequest("/v1/status", void 0, root);
|
|
21454
22172
|
throw new Error("Backify dev is already running for this repository");
|
|
@@ -21478,15 +22196,15 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21478
22196
|
}
|
|
21479
22197
|
throw error;
|
|
21480
22198
|
}
|
|
21481
|
-
const token =
|
|
21482
|
-
const identityScope = services?.identityScope ?? { organizationId:
|
|
22199
|
+
const token = randomBytes6(32).toString("hex");
|
|
22200
|
+
const identityScope = services?.identityScope ?? { organizationId: randomUUID9(), applicationId: randomUUID9(), environmentId: randomUUID9() };
|
|
21483
22201
|
const workerEnvironment = (worker) => {
|
|
21484
22202
|
if (!worker.authTrust) return worker;
|
|
21485
22203
|
const { renewal, ...authTrust } = worker.authTrust;
|
|
21486
22204
|
return { ...worker, authTrust };
|
|
21487
22205
|
};
|
|
21488
22206
|
const identityEnvironment = () => ({ BACKIFY_LOCAL_IDENTITY: identityProfiles ? JSON.stringify({ ...identityProfiles, scope: identityScope, requestOrigins: [backendUrl, frontend?.url].filter(Boolean) }) : "", BACKIFY_LOCAL_JOB_WORKER: jobWorker ? JSON.stringify({ ...workerEnvironment(jobWorker), scope: identityScope, historicalWorkers: historicalWorkers.map(workerEnvironment) }) : "", BACKIFY_LOCAL_JOBS: services?.jobs ? "postgres-jobs" : "snapshot-emulator" });
|
|
21489
|
-
const outfile =
|
|
22207
|
+
const outfile = join12(directory2, "worker.mjs");
|
|
21490
22208
|
let child;
|
|
21491
22209
|
const trustRenewal = createLocalJobTrustRenewal(root, authMounts, { onFailure: () => console.error("Native worker trust renewal unavailable; existing validity and pin guards remain enforced."), onUpdate: () => {
|
|
21492
22210
|
if (child?.connected) child.send({ type: "trust-renewed", workers: activeWorkers.filter((worker) => worker.authTrust).map((worker) => ({ artifactVersion: worker.artifact.artifactVersion, revision: worker.authTrust.revision, trustDigest: worker.authTrust.trustDigest })) }, (error) => {
|
|
@@ -21524,8 +22242,8 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21524
22242
|
}
|
|
21525
22243
|
await stopChild(child);
|
|
21526
22244
|
if (stopping) return;
|
|
21527
|
-
const version =
|
|
21528
|
-
const worker = spawn4(process.execPath, [outfile], { cwd: root, windowsHide: true, stdio: ["ignore", 2, 2, "ipc"], env: { ...process.env, ...identityEnvironment(), BACKIFY_LOCAL_SECRETS_IPC: "1", HOST: "127.0.0.1", ...backendPort === void 0 ? {} : { PORT: String(backendPort) }, BACKIFY_LOCAL_BACKEND_URL: backendUrl ?? "", BACKIFY_LOCAL_FRONTEND_URL: frontend?.url ?? "", BACKIFY_LOCAL_DATABASE_ENABLED: manifest.capabilities.database && manifest.capabilities.database.enabled !== false ? "1" : "0", BACKIFY_LOCAL_STORAGE_ENABLED: manifest.capabilities.storage && manifest.capabilities.storage.enabled !== false ? "1" : "0", BACKIFY_LOCAL_STORAGE_BUCKETS: JSON.stringify(manifest.capabilities.storage?.buckets?.map((bucket) => bucket.name) ?? (manifest.capabilities.storage ? ["default"] : [])), BACKIFY_LOCAL_TOKEN: token, BACKIFY_LOCAL_PORT: String(dashboardPort), BACKIFY_LOCAL_STATE:
|
|
22245
|
+
const version = createHash13("sha256").update(readFileSync18(outfile)).update(services?.configurationFingerprint ?? "lightweight").digest("hex");
|
|
22246
|
+
const worker = spawn4(process.execPath, [outfile], { cwd: root, windowsHide: true, stdio: ["ignore", 2, 2, "ipc"], env: { ...process.env, ...identityEnvironment(), BACKIFY_LOCAL_SECRETS_IPC: "1", HOST: "127.0.0.1", ...backendPort === void 0 ? {} : { PORT: String(backendPort) }, BACKIFY_LOCAL_BACKEND_URL: backendUrl ?? "", BACKIFY_LOCAL_FRONTEND_URL: frontend?.url ?? "", BACKIFY_LOCAL_DATABASE_ENABLED: manifest.capabilities.database && manifest.capabilities.database.enabled !== false ? "1" : "0", BACKIFY_LOCAL_STORAGE_ENABLED: manifest.capabilities.storage && manifest.capabilities.storage.enabled !== false ? "1" : "0", BACKIFY_LOCAL_STORAGE_BUCKETS: JSON.stringify(manifest.capabilities.storage?.buckets?.map((bucket) => bucket.name) ?? (manifest.capabilities.storage ? ["default"] : [])), BACKIFY_LOCAL_TOKEN: token, BACKIFY_LOCAL_PORT: String(dashboardPort), BACKIFY_LOCAL_STATE: join12(directory2, "state.json"), BACKIFY_LOCAL_VERSION: version, BACKIFY_LOCAL_HCR_URL: services?.runtimeUrl ?? "", BACKIFY_LOCAL_WORKLOAD_TOKEN_PATH: services?.tokenPath ?? "", BACKIFY_LOCAL_DATABASE: services ? services.database ? "postgres" : "disabled" : "document-emulator", BACKIFY_LOCAL_EVENTS: services?.events ? "postgres-events" : services ? "disabled" : "snapshot-emulator", BACKIFY_LOCAL_STORAGE: services ? services.storage ? "filesystem" : "disabled" : "snapshot-emulator" } });
|
|
21529
22247
|
child = worker;
|
|
21530
22248
|
worker.send({ type: "local-secrets", secrets }, (error) => {
|
|
21531
22249
|
if (error) {
|
|
@@ -21547,7 +22265,7 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21547
22265
|
workerReady = true;
|
|
21548
22266
|
const session = { version: 1, port: message.port, token, pid: worker.pid };
|
|
21549
22267
|
const temp = sessionPath(root) + ".tmp";
|
|
21550
|
-
|
|
22268
|
+
writeFileSync10(temp, JSON.stringify(session), { mode: 384 });
|
|
21551
22269
|
renameLocalFile(temp, sessionPath(root));
|
|
21552
22270
|
const url = "http://127.0.0.1:" + message.port;
|
|
21553
22271
|
options.onReady?.(url, { frontendUrl: frontend?.url, backendUrl });
|
|
@@ -21559,17 +22277,17 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21559
22277
|
});
|
|
21560
22278
|
};
|
|
21561
22279
|
try {
|
|
21562
|
-
buildContext = await context2({ absWorkingDir: root, stdin: { contents: localWorkerSource(
|
|
22280
|
+
buildContext = await context2({ absWorkingDir: root, stdin: { contents: localWorkerSource(resolve17(root, manifest.runtime.entrypoint)), resolveDir: root, sourcefile: "backify-local-worker.ts", loader: "ts" }, bundle: true, platform: "node", format: "esm", target: "node20", outfile, sourcemap: true, banner: { js: 'import { createRequire as __backifyCreateRequire } from "node:module"; var require = __backifyCreateRequire(import.meta.url);' }, plugins: [{ name: "backify-local-lifecycle", setup(build3) {
|
|
21563
22281
|
build3.onLoad({ filter: /./, namespace: "file" }, (input) => {
|
|
21564
|
-
if (input.path !==
|
|
22282
|
+
if (input.path !== resolve17(root, manifest.runtime.entrypoint)) return;
|
|
21565
22283
|
try {
|
|
21566
22284
|
const updated = localManifest(root);
|
|
21567
22285
|
if (services && JSON.stringify(updated.capabilities) !== JSON.stringify(manifest.capabilities)) throw new Error("Full-fidelity capability or migration intent changed; restart backify dev to review and apply it");
|
|
21568
22286
|
if (updated.runtime.entrypoint !== manifest.runtime.entrypoint) throw new Error("Runtime entrypoint changed; restart backify dev");
|
|
21569
22287
|
} catch (error) {
|
|
21570
|
-
return { errors: [{ text: error instanceof Error ? error.message : "Manifest validation failed" }], watchFiles: [
|
|
22288
|
+
return { errors: [{ text: error instanceof Error ? error.message : "Manifest validation failed" }], watchFiles: [join12(root, "backify.yaml")] };
|
|
21571
22289
|
}
|
|
21572
|
-
return { contents:
|
|
22290
|
+
return { contents: readFileSync18(input.path, "utf8"), loader: input.path.endsWith(".tsx") ? "tsx" : /\.(ts|mts|cts)$/.test(input.path) ? "ts" : input.path.endsWith(".jsx") ? "jsx" : "js", watchFiles: [join12(root, "backify.yaml")] };
|
|
21573
22291
|
});
|
|
21574
22292
|
build3.onEnd((result) => {
|
|
21575
22293
|
if (result.errors.length) rejectStartup(new Error("Local application build failed"));
|
|
@@ -21625,8 +22343,8 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21625
22343
|
}
|
|
21626
22344
|
}
|
|
21627
22345
|
}
|
|
21628
|
-
if (
|
|
21629
|
-
const session = JSON.parse(
|
|
22346
|
+
if (existsSync11(sessionPath(root))) {
|
|
22347
|
+
const session = JSON.parse(readFileSync18(sessionPath(root), "utf8"));
|
|
21630
22348
|
if (session.token === token) unlinkSync4(sessionPath(root));
|
|
21631
22349
|
}
|
|
21632
22350
|
};
|
|
@@ -21640,11 +22358,11 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21640
22358
|
}
|
|
21641
22359
|
async function runLocalTests(root = process.cwd()) {
|
|
21642
22360
|
const manifest = localManifest(root);
|
|
21643
|
-
const project = JSON.parse(
|
|
22361
|
+
const project = JSON.parse(readFileSync18(join12(root, "package.json"), "utf8"));
|
|
21644
22362
|
if (!project.scripts?.test) throw new Error("Add a test script to package.json so AI agents can run backify test");
|
|
21645
22363
|
const result = await new Promise((resolveResult, reject) => {
|
|
21646
|
-
const npmPath = process.platform === "win32" ? process.env.npm_execpath ??
|
|
21647
|
-
if (npmPath && !
|
|
22364
|
+
const npmPath = process.platform === "win32" ? process.env.npm_execpath ?? join12(dirname12(process.execPath), "node_modules", "npm", "bin", "npm-cli.js") : void 0;
|
|
22365
|
+
if (npmPath && !existsSync11(npmPath)) {
|
|
21648
22366
|
reject(new Error("Cannot locate npm-cli.js; run backify test through npx or npm"));
|
|
21649
22367
|
return;
|
|
21650
22368
|
}
|
|
@@ -21664,15 +22382,15 @@ async function runLocalTests(root = process.cwd()) {
|
|
|
21664
22382
|
// src/workload.ts
|
|
21665
22383
|
function inside3(root, filename2) {
|
|
21666
22384
|
const part = relative8(root, filename2);
|
|
21667
|
-
return part !== ".." && !part.startsWith("../") && !part.startsWith("..\\") && !
|
|
22385
|
+
return part !== ".." && !part.startsWith("../") && !part.startsWith("..\\") && !isAbsolute14(part);
|
|
21668
22386
|
}
|
|
21669
22387
|
function digest3(contents) {
|
|
21670
|
-
return
|
|
22388
|
+
return createHash14("sha256").update(contents).digest("hex");
|
|
21671
22389
|
}
|
|
21672
22390
|
function packageDirectory(filename2) {
|
|
21673
|
-
for (let current =
|
|
21674
|
-
if (
|
|
21675
|
-
if (
|
|
22391
|
+
for (let current = dirname13(filename2); ; current = dirname13(current)) {
|
|
22392
|
+
if (existsSync12(join13(current, "package.json"))) return current;
|
|
22393
|
+
if (dirname13(current) === current) throw new Error("Cannot locate an installed dependency package");
|
|
21676
22394
|
}
|
|
21677
22395
|
}
|
|
21678
22396
|
function workloadSource(entry, kind, capabilities) {
|
|
@@ -21753,11 +22471,11 @@ main().catch(() => finish({ schemaVersion: 1, status: "failed", error: "workload
|
|
|
21753
22471
|
`.replace("__KIND__", JSON.stringify(kind)).replace("__CAPABILITIES__", JSON.stringify(capabilities)).replace("__AUTH_ENABLED__", String(capabilities.includes("auth"))).replace("__ENTRY__", JSON.stringify(entry));
|
|
21754
22472
|
}
|
|
21755
22473
|
async function prepareWorkload(options) {
|
|
21756
|
-
const root = realpathSync6(
|
|
22474
|
+
const root = realpathSync6(resolve18(options.root ?? process.cwd()));
|
|
21757
22475
|
const manifest = localManifest(root);
|
|
21758
22476
|
if (options.kind !== "http" && options.kind !== "worker") throw new Error("Workload kind must be http or worker");
|
|
21759
22477
|
if (options.kind === "worker" && !options.entry) throw new Error("Workers require an explicit declaration-only entrypoint");
|
|
21760
|
-
const entry = realpathSync6(
|
|
22478
|
+
const entry = realpathSync6(resolve18(root, options.entry ?? manifest.runtime.entrypoint));
|
|
21761
22479
|
if (!inside3(root, entry)) throw new Error("Workload entrypoint must stay inside this repository");
|
|
21762
22480
|
const enabled = Object.entries(manifest.capabilities).filter(([, settings]) => settings?.enabled !== false).map(([name2]) => name2).sort();
|
|
21763
22481
|
if (options.kind === "worker" && !enabled.includes("jobs")) throw new Error("Workers require declared jobs capability");
|
|
@@ -21783,14 +22501,14 @@ async function prepareWorkload(options) {
|
|
|
21783
22501
|
context3.onResolve({ filter: /./ }, async (input) => {
|
|
21784
22502
|
if (input.pluginData?.backifyResolved) return;
|
|
21785
22503
|
if (input.path === "@backifydev/sdk/local" || input.path === "@backifydev/sdk/runtime" && input.namespace !== "backify-workload-host") return { errors: [{ text: "Production application code must not import local emulators or host-only runtime configuration" }] };
|
|
21786
|
-
const resolved = await context3.resolve(input.path, { importer: input.namespace === "backify-workload-host" ?
|
|
22504
|
+
const resolved = await context3.resolve(input.path, { importer: input.namespace === "backify-workload-host" ? join13(root, input.importer) : input.importer, resolveDir: input.resolveDir, kind: input.kind, pluginData: { backifyResolved: true } });
|
|
21787
22505
|
if (!resolved.external && !resolved.errors.length && resolved.path) {
|
|
21788
22506
|
const directory2 = realpathSync6(packageDirectory(resolved.path));
|
|
21789
22507
|
const target = relative8(directory2, realpathSync6(resolved.path)).replaceAll("\\", "/");
|
|
21790
|
-
if (JSON.parse(
|
|
22508
|
+
if (JSON.parse(readFileSync19(join13(directory2, "package.json"), "utf8")).name === "@backifydev/sdk") {
|
|
21791
22509
|
if (/^(src|dist)\/local\.(ts|js)$/.test(target) || /^(src|dist)\/runtime\.(ts|js)$/.test(target) && input.namespace !== "backify-workload-host" && !inside3(directory2, realpathSync6(input.importer))) return { errors: [{ text: "Production application code must not import local emulators or host-only runtime configuration" }] };
|
|
21792
22510
|
}
|
|
21793
|
-
if (!
|
|
22511
|
+
if (!isAbsolute14(input.path) && !input.path.startsWith(".")) trustedPackages.add(directory2);
|
|
21794
22512
|
}
|
|
21795
22513
|
return resolved;
|
|
21796
22514
|
});
|
|
@@ -21849,34 +22567,34 @@ async function packageWorkload(options) {
|
|
|
21849
22567
|
if (plan.blockers.includes("bundle-warning-review-required")) throw new Error("Workload bundle warnings require review before packaging");
|
|
21850
22568
|
const dockerfile = ["FROM " + options.baseImage, "WORKDIR /app", "COPY --chown=1000:1000 workload.mjs artifact.json ./", "ENV NODE_ENV=production PORT=8080 BACKIFY_ARTIFACT_SHA256=" + plan.artifactVersion + " BACKIFY_WORKLOAD_TOKEN_FILE=/run/backify/workload/token", "USER 1000:1000", ...options.kind === "http" ? ["EXPOSE 8080"] : [], "STOPSIGNAL SIGTERM", 'ENTRYPOINT ["node", "workload.mjs"]', ""].join("\n");
|
|
21851
22569
|
const files = { "workload.mjs": bundle, "artifact.json": JSON.stringify(plan, null, 2) + "\n", "Dockerfile": dockerfile, ".dockerignore": "**\n!Dockerfile\n!workload.mjs\n!artifact.json\n!.dockerignore\n" };
|
|
21852
|
-
const output =
|
|
22570
|
+
const output = resolve18(root, options.output ?? ".backify/artifacts/" + options.kind + "-" + plan.artifactVersion.slice(0, 16));
|
|
21853
22571
|
assertLocalStatePath(root, output);
|
|
21854
|
-
if (
|
|
21855
|
-
if (JSON.stringify(
|
|
21856
|
-
assertLocalStatePath(root,
|
|
21857
|
-
return !
|
|
22572
|
+
if (existsSync12(output)) {
|
|
22573
|
+
if (JSON.stringify(readdirSync6(output).sort()) !== JSON.stringify(Object.keys(files).sort()) || Object.entries(files).some(([name2, value]) => {
|
|
22574
|
+
assertLocalStatePath(root, join13(output, name2));
|
|
22575
|
+
return !readFileSync19(join13(output, name2)).equals(Buffer.from(value));
|
|
21858
22576
|
})) throw new Error("Refusing to overwrite an existing workload artifact directory");
|
|
21859
22577
|
} else {
|
|
21860
|
-
|
|
21861
|
-
for (const [name2, value] of Object.entries(files))
|
|
22578
|
+
mkdirSync7(output, { recursive: true });
|
|
22579
|
+
for (const [name2, value] of Object.entries(files)) writeFileSync11(join13(output, name2), value, { flag: "wx", mode: 384 });
|
|
21862
22580
|
}
|
|
21863
22581
|
return { ...plan, outputDirectory: relative8(root, output).replaceAll("\\", "/") };
|
|
21864
22582
|
}
|
|
21865
22583
|
|
|
21866
22584
|
// src/workload-provenance.ts
|
|
21867
22585
|
import { execFileSync } from "node:child_process";
|
|
21868
|
-
import { mkdirSync as
|
|
22586
|
+
import { mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync2, readdirSync as readdirSync7, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "node:fs";
|
|
21869
22587
|
import { tmpdir } from "node:os";
|
|
21870
|
-
import { basename as
|
|
22588
|
+
import { basename as basename3, dirname as dirname15, isAbsolute as isAbsolute15, join as join15, relative as relative9, resolve as resolve20 } from "node:path";
|
|
21871
22589
|
|
|
21872
22590
|
// src/workload-provenance-oci.ts
|
|
21873
|
-
import { createHash as
|
|
21874
|
-
import { createReadStream, lstatSync as
|
|
21875
|
-
import { dirname as
|
|
22591
|
+
import { createHash as createHash15 } from "node:crypto";
|
|
22592
|
+
import { createReadStream, lstatSync as lstatSync16, readFileSync as readFileSync20 } from "node:fs";
|
|
22593
|
+
import { dirname as dirname14, join as join14, parse as parse3, resolve as resolve19 } from "node:path";
|
|
21876
22594
|
import { Transform, Writable } from "node:stream";
|
|
21877
22595
|
import { pipeline } from "node:stream/promises";
|
|
21878
22596
|
import { createGunzip } from "node:zlib";
|
|
21879
|
-
var provenanceHash = (bytes) => "sha256:" +
|
|
22597
|
+
var provenanceHash = (bytes) => "sha256:" + createHash15("sha256").update(bytes).digest("hex");
|
|
21880
22598
|
var maximumLayer = 256 * 1024 * 1024;
|
|
21881
22599
|
var maximumImage = 1024 * 1024 * 1024;
|
|
21882
22600
|
var manifestTypes = ["application/vnd.oci.image.manifest.v1+json", "application/vnd.docker.distribution.manifest.v2+json"];
|
|
@@ -21887,14 +22605,14 @@ function provenanceRef(value) {
|
|
|
21887
22605
|
return value.slice(value.lastIndexOf("@") + 1);
|
|
21888
22606
|
}
|
|
21889
22607
|
function provenanceFile(filename2, maximum) {
|
|
21890
|
-
const absolute =
|
|
21891
|
-
for (let ancestor = absolute; ; ancestor =
|
|
21892
|
-
if (
|
|
22608
|
+
const absolute = resolve19(filename2);
|
|
22609
|
+
for (let ancestor = absolute; ; ancestor = dirname14(ancestor)) {
|
|
22610
|
+
if (lstatSync16(ancestor).isSymbolicLink()) throw new Error("Provenance paths must not contain links");
|
|
21893
22611
|
if (ancestor === parse3(absolute).root) break;
|
|
21894
22612
|
}
|
|
21895
|
-
const stat =
|
|
22613
|
+
const stat = lstatSync16(absolute);
|
|
21896
22614
|
if (!stat.isFile() || stat.nlink !== 1 || stat.size < 1 || stat.size > maximum) throw new Error("Invalid provenance file");
|
|
21897
|
-
const bytes =
|
|
22615
|
+
const bytes = readFileSync20(absolute);
|
|
21898
22616
|
if (bytes.length !== stat.size) throw new Error("Provenance file changed during read");
|
|
21899
22617
|
return bytes;
|
|
21900
22618
|
}
|
|
@@ -21903,7 +22621,7 @@ function descriptor(value) {
|
|
|
21903
22621
|
return value;
|
|
21904
22622
|
}
|
|
21905
22623
|
function blobPath(root, value) {
|
|
21906
|
-
return
|
|
22624
|
+
return join14(root, "blobs", "sha256", descriptor(value).digest.slice(7));
|
|
21907
22625
|
}
|
|
21908
22626
|
function jsonBlob(root, value) {
|
|
21909
22627
|
const bytes = provenanceFile(blobPath(root, value), 1024 * 1024);
|
|
@@ -21912,8 +22630,8 @@ function jsonBlob(root, value) {
|
|
|
21912
22630
|
}
|
|
21913
22631
|
async function inspectProvenanceImage(root, architecture, requestedDigest) {
|
|
21914
22632
|
if (!["amd64", "arm64"].includes(architecture) || requestedDigest && !digestPattern.test(requestedDigest)) throw new Error("Invalid OCI platform or digest");
|
|
21915
|
-
if (JSON.parse(provenanceFile(
|
|
21916
|
-
const indexBytes = provenanceFile(
|
|
22633
|
+
if (JSON.parse(provenanceFile(join14(root, "oci-layout"), 1024).toString()).imageLayoutVersion !== "1.0.0") throw new Error("OCI layout required");
|
|
22634
|
+
const indexBytes = provenanceFile(join14(root, "index.json"), 1024 * 1024);
|
|
21917
22635
|
let current = JSON.parse(indexBytes.toString());
|
|
21918
22636
|
let selected;
|
|
21919
22637
|
let manifestDigest = provenanceHash(indexBytes);
|
|
@@ -21944,7 +22662,7 @@ async function inspectProvenanceImage(root, architecture, requestedDigest) {
|
|
|
21944
22662
|
if (!digestPattern.test(config.rootfs.diff_ids[index])) throw new Error("Invalid OCI diff ID");
|
|
21945
22663
|
const filename2 = blobPath(root, layer);
|
|
21946
22664
|
provenanceFileMetadata(filename2, layer.size);
|
|
21947
|
-
const storedHash =
|
|
22665
|
+
const storedHash = createHash15("sha256"), expandedHash = createHash15("sha256");
|
|
21948
22666
|
let stored = 0, expanded = 0;
|
|
21949
22667
|
const counter = new Transform({ transform(chunk, _encoding, callback) {
|
|
21950
22668
|
stored += chunk.length;
|
|
@@ -21970,12 +22688,12 @@ async function inspectProvenanceImage(root, architecture, requestedDigest) {
|
|
|
21970
22688
|
return { manifestDigest, configDigest: configuration.digest, architecture, layers };
|
|
21971
22689
|
}
|
|
21972
22690
|
function provenanceFileMetadata(filename2, expectedSize) {
|
|
21973
|
-
const absolute =
|
|
21974
|
-
for (let ancestor = absolute; ; ancestor =
|
|
21975
|
-
if (
|
|
22691
|
+
const absolute = resolve19(filename2);
|
|
22692
|
+
for (let ancestor = absolute; ; ancestor = dirname14(ancestor)) {
|
|
22693
|
+
if (lstatSync16(ancestor).isSymbolicLink()) throw new Error("Provenance paths must not contain links");
|
|
21976
22694
|
if (ancestor === parse3(absolute).root) break;
|
|
21977
22695
|
}
|
|
21978
|
-
const stat =
|
|
22696
|
+
const stat = lstatSync16(absolute);
|
|
21979
22697
|
if (!stat.isFile() || stat.nlink !== 1 || stat.size !== expectedSize) throw new Error("Invalid OCI layer file");
|
|
21980
22698
|
}
|
|
21981
22699
|
function verifyBaseLineage(base, image2) {
|
|
@@ -21998,7 +22716,7 @@ function canonical(value) {
|
|
|
21998
22716
|
if (value && typeof value === "object") return "{" + Object.entries(value).sort(([left], [right]) => left.localeCompare(right, "en")).map(([name2, item]) => JSON.stringify(name2) + ":" + canonical(item)).join(",") + "}";
|
|
21999
22717
|
return JSON.stringify(value);
|
|
22000
22718
|
}
|
|
22001
|
-
function
|
|
22719
|
+
function git2(root, args2) {
|
|
22002
22720
|
try {
|
|
22003
22721
|
return execFileSync("git", ["-C", root, ...args2], { encoding: "utf8", windowsHide: true, timeout: 3e4, maxBuffer: 16 * 1024 * 1024 }).trim();
|
|
22004
22722
|
} catch {
|
|
@@ -22033,28 +22751,28 @@ function dependencyMaterials(lock) {
|
|
|
22033
22751
|
return { digest: provenanceHash(canonical(materials.sort((left, right) => left.path.localeCompare(right.path, "en")))), count: materials.length };
|
|
22034
22752
|
}
|
|
22035
22753
|
async function captureWorkloadProvenance(options) {
|
|
22036
|
-
const root =
|
|
22754
|
+
const root = resolve20(options.root ?? process.cwd());
|
|
22037
22755
|
if (!repositoryPattern.test(options.repository) || !["first", "second"].includes(options.replica)) throw new Error("Invalid provenance repository or replica");
|
|
22038
|
-
const applicationPath = relative9(root,
|
|
22039
|
-
if (!applicationPath ||
|
|
22040
|
-
if (
|
|
22041
|
-
const commit =
|
|
22756
|
+
const applicationPath = relative9(root, resolve20(root, options.application)).replaceAll("\\", "/");
|
|
22757
|
+
if (!applicationPath || isAbsolute15(applicationPath) || applicationPath.split("/").includes("..")) throw new Error("Application must be inside the source checkout");
|
|
22758
|
+
if (git2(root, ["rev-parse", "--show-toplevel"]).replaceAll("\\", "/").toLowerCase() !== root.replaceAll("\\", "/").toLowerCase() || git2(root, ["status", "--porcelain", "--untracked-files=all"]) || git2(root, ["ls-files", "--stage"]).split("\n").some((line) => line.startsWith("160000 "))) throw new Error("Clean root checkout without submodules required");
|
|
22759
|
+
const commit = git2(root, ["rev-parse", "HEAD"]), tree = git2(root, ["rev-parse", "HEAD^{tree}"]);
|
|
22042
22760
|
if (!sha.test(commit) || !sha.test(tree)) throw new Error("Full Git commit and tree required");
|
|
22043
|
-
const origin =
|
|
22761
|
+
const origin = git2(root, ["remote", "get-url", "origin"]);
|
|
22044
22762
|
if (!["https://github.com/" + options.repository, "https://github.com/" + options.repository + ".git", "git@github.com:" + options.repository + ".git"].includes(origin)) throw new Error("Source repository does not match origin");
|
|
22045
|
-
const lockBytes = provenanceFile(
|
|
22763
|
+
const lockBytes = provenanceFile(join15(root, "package-lock.json"), 16 * 1024 * 1024);
|
|
22046
22764
|
const dependencies = dependencyMaterials(JSON.parse(lockBytes.toString("utf8")));
|
|
22047
|
-
const context3 =
|
|
22048
|
-
if (canonical(
|
|
22765
|
+
const context3 = resolve20(root, options.context);
|
|
22766
|
+
if (canonical(readdirSync7(context3).sort()) !== canonical(contextNames)) throw new Error("Exact allowlisted workload context required");
|
|
22049
22767
|
const contextDigests = {};
|
|
22050
|
-
for (const name2 of contextNames) contextDigests[name2] = provenanceHash(provenanceFile(
|
|
22051
|
-
const plan = boundedJson(
|
|
22768
|
+
for (const name2 of contextNames) contextDigests[name2] = provenanceHash(provenanceFile(join15(context3, name2), name2 === "workload.mjs" ? 16 * 1024 * 1024 : 65536));
|
|
22769
|
+
const plan = boundedJson(join15(context3, "artifact.json"));
|
|
22052
22770
|
const baseDigest = provenanceRef(options.baseImage);
|
|
22053
22771
|
if (plan.schemaVersion !== 1 || plan.kind !== "worker" || plan.baseImage !== options.baseImage || !/^[a-f0-9]{64}$/.test(plan.artifactVersion) || !/^[a-f0-9]{64}$/.test(plan.manifestVersion) || contextDigests["workload.mjs"] !== "sha256:" + plan.artifactVersion || typeof plan.entrypoint !== "string") throw new Error("Worker context does not match artifact metadata");
|
|
22054
22772
|
const dockerfile = ["FROM " + options.baseImage, "WORKDIR /app", "COPY --chown=1000:1000 workload.mjs artifact.json ./", "ENV NODE_ENV=production PORT=8080 BACKIFY_ARTIFACT_SHA256=" + plan.artifactVersion + " BACKIFY_WORKLOAD_TOKEN_FILE=/run/backify/workload/token", "USER 1000:1000", "STOPSIGNAL SIGTERM", 'ENTRYPOINT ["node", "workload.mjs"]', ""].join("\n");
|
|
22055
|
-
if (!provenanceFile(
|
|
22056
|
-
const base = await inspectProvenanceImage(
|
|
22057
|
-
const image2 = await inspectProvenanceImage(
|
|
22773
|
+
if (!provenanceFile(join15(context3, "Dockerfile"), 65536).equals(Buffer.from(dockerfile)) || provenanceFile(join15(context3, ".dockerignore"), 65536).toString() !== "**\n!Dockerfile\n!workload.mjs\n!artifact.json\n!.dockerignore\n") throw new Error("Unreviewed workload build recipe");
|
|
22774
|
+
const base = await inspectProvenanceImage(resolve20(root, options.baseLayout), options.architecture, baseDigest);
|
|
22775
|
+
const image2 = await inspectProvenanceImage(resolve20(root, options.imageLayout), options.architecture);
|
|
22058
22776
|
verifyBaseLineage(base, image2);
|
|
22059
22777
|
let npm;
|
|
22060
22778
|
try {
|
|
@@ -22076,7 +22794,7 @@ function validateReport(report) {
|
|
|
22076
22794
|
if (report.schemaVersion !== 1 || report.kind !== "worker-build-provenance" || !repositoryPattern.test(report.source.repository) || !sha.test(report.source.commit) || !sha.test(report.source.tree) || !digestPattern.test(report.source.lockfileDigest) || !digestPattern.test(report.source.dependencyMaterialsDigest) || !Number.isSafeInteger(report.source.dependencyCount) || report.source.dependencyCount < 1 || !/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(report.toolchain.node) || !/^[0-9]+\.[0-9]+\.[0-9]+$/.test(report.toolchain.npm) || !/^[a-f0-9]{64}$/.test(report.build.artifactVersion) || !/^[a-f0-9]{64}$/.test(report.build.manifestVersion) || Object.values(report.build.context).some((value) => !digestPattern.test(value)) || report.build.context["workload.mjs"] !== "sha256:" + report.build.artifactVersion || !/^[1-9][0-9]*$/.test(report.invocation.runId) || !/^[1-9][0-9]*$/.test(report.invocation.attempt) || !["first", "second"].includes(report.invocation.replica)) throw new Error("Invalid build provenance report");
|
|
22077
22795
|
for (const path of [report.build.applicationPath, report.build.entry]) {
|
|
22078
22796
|
text2(path);
|
|
22079
|
-
if (
|
|
22797
|
+
if (isAbsolute15(path) || path.includes(":") || path.includes("\\") || path.split("/").includes("..")) throw new Error("Invalid build source path");
|
|
22080
22798
|
}
|
|
22081
22799
|
provenanceRef(report.build.baseImage);
|
|
22082
22800
|
for (const image2 of [report.base, report.image]) {
|
|
@@ -22127,17 +22845,17 @@ function verifyWorkloadProvenance(options, now = /* @__PURE__ */ new Date()) {
|
|
|
22127
22845
|
if (provenanceHash(baseSubject) !== provenanceRef(policy.baseImage)) throw new Error("Base attestation subject does not match the approved image digest");
|
|
22128
22846
|
evidence.push({ bytes: baseSubject, bundle: provenanceFile(options.baseBundle, 4 * 1024 * 1024), identity: { ...policy, ...supplier } });
|
|
22129
22847
|
}
|
|
22130
|
-
const scratchRoot =
|
|
22131
|
-
const scratch =
|
|
22132
|
-
if (
|
|
22848
|
+
const scratchRoot = resolve20(tmpdir());
|
|
22849
|
+
const scratch = mkdtempSync2(join15(scratchRoot, "backify-provenance-"));
|
|
22850
|
+
if (dirname15(scratch) !== scratchRoot || !basename3(scratch).startsWith("backify-provenance-")) throw new Error("Unsafe provenance evidence directory");
|
|
22133
22851
|
try {
|
|
22134
|
-
|
|
22135
|
-
const trustedRoot =
|
|
22136
|
-
|
|
22852
|
+
mkdirSync8(join15(scratch, "evidence"), { mode: 448 });
|
|
22853
|
+
const trustedRoot = join15(scratch, "evidence", "trusted-root.jsonl");
|
|
22854
|
+
writeFileSync12(trustedRoot, rootBytes, { flag: "wx", mode: 384 });
|
|
22137
22855
|
for (const [index, { bytes, bundle: bundleBytes, identity }] of evidence.entries()) {
|
|
22138
|
-
const report =
|
|
22139
|
-
|
|
22140
|
-
|
|
22856
|
+
const report = join15(scratch, "evidence", "report-" + index + ".json"), bundle = join15(scratch, "evidence", "bundle-" + index + ".jsonl");
|
|
22857
|
+
writeFileSync12(report, bytes, { flag: "wx", mode: 384 });
|
|
22858
|
+
writeFileSync12(bundle, bundleBytes, { flag: "wx", mode: 384 });
|
|
22141
22859
|
try {
|
|
22142
22860
|
const result = execFileSync("gh", ["attestation", "verify", report, "--bundle", bundle, "--custom-trusted-root", trustedRoot, "--hostname", "github.com", "--repo", identity.repository, "--signer-workflow", identity.signerWorkflow, "--signer-digest", identity.signerDigest, "--source-digest", identity.sourceCommit, "--source-ref", identity.sourceRef, "--deny-self-hosted-runners", "--predicate-type", "https://slsa.dev/provenance/v1", "--format", "json"], { encoding: "utf8", windowsHide: true, timeout: 12e4, maxBuffer: 4 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
22143
22861
|
if (!Array.isArray(JSON.parse(result)) || !JSON.parse(result).length) throw new Error();
|
|
@@ -22146,7 +22864,7 @@ function verifyWorkloadProvenance(options, now = /* @__PURE__ */ new Date()) {
|
|
|
22146
22864
|
}
|
|
22147
22865
|
}
|
|
22148
22866
|
} finally {
|
|
22149
|
-
|
|
22867
|
+
rmSync2(scratch, { recursive: true, force: true });
|
|
22150
22868
|
}
|
|
22151
22869
|
return { schemaVersion: 1, status: "build-provenance-verified", sourceCommitVerified: true, dependencyMaterialsVerified: true, dependencyReproductionVerified: Boolean(second), imageReproductionVerified: Boolean(second), baseDigestPinned: true, baseLineageVerified: true, baseSupplierProvenanceVerified: Boolean(supplier), baseImageVerified: Boolean(supplier), imageContentVerified: false, deploymentReady: false, verifiedAt: now.toISOString(), expiresAt: policy.expiresAt, policyDigest: provenanceHash(policyBytes), sourceCommit: policy.sourceCommit, imageDigest: first.image.manifestDigest, artifactVersion: first.build.artifactVersion, manifestVersion: first.build.manifestVersion, reportDigests: reportBytes.map(provenanceHash), blockers: ["separate-hcr-image-content-verification-required", "base-runtime-job-conformance-required", "reviewed-signed-fleet-installation-required", "production-workload-credential-isolation-required"] };
|
|
22152
22870
|
}
|
|
@@ -22168,13 +22886,13 @@ async function workloadProvenanceCommand(args2) {
|
|
|
22168
22886
|
return verifyWorkloadProvenance({ policy: required("--policy"), firstReport: options.get("--report") ?? required("--first-report"), firstBundle: options.get("--bundle") ?? required("--first-bundle"), secondReport: options.get("--second-report"), secondBundle: options.get("--second-bundle"), baseSubject: options.get("--base-subject"), baseBundle: options.get("--base-bundle"), trustedRoot: required("--trusted-root") });
|
|
22169
22887
|
}
|
|
22170
22888
|
const result = await captureWorkloadProvenance({ root: options.get("--root"), repository: required("--repository"), application: required("--application"), context: required("--context"), baseLayout: required("--base-layout"), imageLayout: required("--image-layout"), baseImage: required("--base-image"), architecture: required("--architecture"), replica: options.get("--replica") ?? "first" });
|
|
22171
|
-
|
|
22889
|
+
writeFileSync12(resolve20(required("--output")), JSON.stringify(result, null, 2) + "\n", { flag: "wx", mode: 384 });
|
|
22172
22890
|
return { schemaVersion: 1, status: "captured-unsigned", deploymentReady: false, reportDigest: provenanceHash(JSON.stringify(result, null, 2) + "\n") };
|
|
22173
22891
|
}
|
|
22174
22892
|
|
|
22175
22893
|
// src/workload-artifact-plan.ts
|
|
22176
|
-
import { existsSync as
|
|
22177
|
-
import { resolve as
|
|
22894
|
+
import { existsSync as existsSync13, lstatSync as lstatSync17, readFileSync as readFileSync21, realpathSync as realpathSync7 } from "node:fs";
|
|
22895
|
+
import { resolve as resolve21 } from "node:path";
|
|
22178
22896
|
function uuid4(value) {
|
|
22179
22897
|
return typeof value === "string" && value.length === 36 && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value) && value !== "00000000-0000-0000-0000-000000000000";
|
|
22180
22898
|
}
|
|
@@ -22212,14 +22930,14 @@ function parseWorkerArtifactPlan(args2) {
|
|
|
22212
22930
|
}
|
|
22213
22931
|
async function planWorkerArtifactRegistration(options) {
|
|
22214
22932
|
if (![options.organizationId, options.applicationId, options.environmentId, options.deploymentId].every(uuid4) || typeof options.sourceCommit !== "string" || ![40, 64].includes(options.sourceCommit.length) || !/^[a-f0-9]+$/.test(options.sourceCommit) || !validImage(options.imageReference)) throw new Error("Invalid worker artifact scope, source or image pin");
|
|
22215
|
-
const root = realpathSync7(
|
|
22933
|
+
const root = realpathSync7(resolve21(options.root ?? process.cwd()));
|
|
22216
22934
|
if (typeof options.jobsFile !== "string" || !options.jobsFile || typeof options.entry !== "string" || !options.entry) throw new Error("Worker artifact planning requires explicit jobs and entry files");
|
|
22217
|
-
const filename2 =
|
|
22935
|
+
const filename2 = resolve21(root, options.jobsFile);
|
|
22218
22936
|
assertLocalStatePath(root, filename2);
|
|
22219
|
-
if (!
|
|
22937
|
+
if (!existsSync13(filename2) || !lstatSync17(filename2).isFile() || lstatSync17(filename2).size > 32768) throw new Error("Worker job metadata file must be a bounded repository file");
|
|
22220
22938
|
let jobs;
|
|
22221
22939
|
try {
|
|
22222
|
-
jobs = jobMetadata(JSON.parse(
|
|
22940
|
+
jobs = jobMetadata(JSON.parse(readFileSync21(filename2, "utf8")));
|
|
22223
22941
|
} catch {
|
|
22224
22942
|
throw new Error("Invalid bounded worker job metadata");
|
|
22225
22943
|
}
|
|
@@ -22240,8 +22958,8 @@ async function planWorkerArtifactRegistration(options) {
|
|
|
22240
22958
|
}
|
|
22241
22959
|
|
|
22242
22960
|
// src/workload-trust-state.ts
|
|
22243
|
-
import { lstatSync as
|
|
22244
|
-
import { isAbsolute as
|
|
22961
|
+
import { lstatSync as lstatSync18, readFileSync as readFileSync22 } from "node:fs";
|
|
22962
|
+
import { isAbsolute as isAbsolute16, resolve as resolve22 } from "node:path";
|
|
22245
22963
|
function parseWorkloadTrustState(args2) {
|
|
22246
22964
|
const action = args2[0];
|
|
22247
22965
|
if (action !== "plan" && action !== "initialize") throw new Error("Use backify workload trust-state plan|initialize");
|
|
@@ -22263,12 +22981,12 @@ function parseWorkloadTrustState(args2) {
|
|
|
22263
22981
|
return { action, trustFile: values.get("--trust-file"), stateFile: values.get("--state-file"), expectedRevision: Number(values.get("--revision")), expectedScope: { organizationId: values.get("--organization"), applicationId: values.get("--application"), environmentId: values.get("--environment") }, confirmed: values.has("--confirm") };
|
|
22264
22982
|
}
|
|
22265
22983
|
function workloadTrustState(options) {
|
|
22266
|
-
if (!["plan", "initialize"].includes(options.action) || !
|
|
22984
|
+
if (!["plan", "initialize"].includes(options.action) || !isAbsolute16(options.trustFile) || !isAbsolute16(options.stateFile) || resolve22(options.trustFile) === resolve22(options.stateFile) || !Number.isSafeInteger(options.expectedRevision) || options.expectedRevision < 1) throw new Error("Specify separate absolute trust/state paths and a positive reviewed revision");
|
|
22267
22985
|
let trust;
|
|
22268
22986
|
try {
|
|
22269
|
-
const metadata =
|
|
22987
|
+
const metadata = lstatSync18(options.trustFile);
|
|
22270
22988
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > 131072) throw new Error();
|
|
22271
|
-
const bytes =
|
|
22989
|
+
const bytes = readFileSync22(options.trustFile);
|
|
22272
22990
|
if (bytes.length > 131072) throw new Error();
|
|
22273
22991
|
trust = JSON.parse(new TextDecoder("utf8", { fatal: true }).decode(bytes));
|
|
22274
22992
|
} catch {
|
|
@@ -22278,14 +22996,14 @@ function workloadTrustState(options) {
|
|
|
22278
22996
|
if (trust.revision !== options.expectedRevision) throw new Error("Application identity trust revision changed; review again");
|
|
22279
22997
|
if (options.action === "initialize") {
|
|
22280
22998
|
if (!options.confirmed) throw new Error("Initialization requires explicit --confirm after reviewing the plan");
|
|
22281
|
-
initializeApplicationAuthState(trust, { expectedScope: options.expectedScope, stateFile:
|
|
22999
|
+
initializeApplicationAuthState(trust, { expectedScope: options.expectedScope, stateFile: resolve22(options.stateFile) });
|
|
22282
23000
|
}
|
|
22283
|
-
return { schemaVersion: 1, status: options.action === "plan" ? "planned" : "initialized", scope: options.expectedScope, revision: trust.revision, validUntil: trust.validUntil, stateFile:
|
|
23001
|
+
return { schemaVersion: 1, status: options.action === "plan" ? "planned" : "initialized", scope: options.expectedScope, revision: trust.revision, validUntil: trust.validUntil, stateFile: resolve22(options.stateFile), requiresPrivatePersistentDirectory: true, recoveryAutomatic: false, productionChanges: false };
|
|
22284
23002
|
}
|
|
22285
23003
|
|
|
22286
23004
|
// src/scaffold.ts
|
|
22287
|
-
import { existsSync as
|
|
22288
|
-
import { dirname as
|
|
23005
|
+
import { existsSync as existsSync14, lstatSync as lstatSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync23, readdirSync as readdirSync8, realpathSync as realpathSync8, writeFileSync as writeFileSync13 } from "node:fs";
|
|
23006
|
+
import { dirname as dirname16, join as join16, relative as relative10, resolve as resolve23 } from "node:path";
|
|
22289
23007
|
function json2(value) {
|
|
22290
23008
|
return JSON.stringify(value, null, 2) + "\n";
|
|
22291
23009
|
}
|
|
@@ -22457,519 +23175,84 @@ server/jobs.ts; it is a declaration-only entry loaded by a trusted Backify host.
|
|
|
22457
23175
|
The Dockerfile builds the HTTP server and web/dist frontend and serves both from
|
|
22458
23176
|
one origin, with HOST/PORT from its host. Keep package-lock.json in source control.
|
|
22459
23177
|
Use backify ship for Preview and promote that tested release to Live. No production
|
|
22460
|
-
provider-readiness claim, cloud deployment or browser authentication is implied.
|
|
22461
|
-
`
|
|
22462
|
-
};
|
|
22463
|
-
}
|
|
22464
|
-
function scaffoldFullStack(options) {
|
|
22465
|
-
const files = fullStackTemplate(options.appId, options.name);
|
|
22466
|
-
const root = resolve21(options.root ?? process.cwd());
|
|
22467
|
-
if (!existsSync11(root) || !lstatSync17(root).isDirectory() || lstatSync17(root).isSymbolicLink()) throw new Error("Scaffold destination must be an existing non-linked directory");
|
|
22468
|
-
const allowed = /* @__PURE__ */ new Set([...Object.keys(files).map((filename2) => filename2.split("/")[0]), ".git", "node_modules", "package-lock.json", ".backify"]);
|
|
22469
|
-
if (readdirSync6(root).some((filename2) => !allowed.has(filename2))) throw new Error("Scaffolding requires an empty folder or an unchanged generated starter");
|
|
22470
|
-
const canonicalRoot = realpathSync8(root);
|
|
22471
|
-
const pending = [];
|
|
22472
|
-
for (const [filename2, content] of Object.entries(files)) {
|
|
22473
|
-
const destination = join13(root, filename2);
|
|
22474
|
-
let current = destination;
|
|
22475
|
-
while (current !== root) {
|
|
22476
|
-
try {
|
|
22477
|
-
const stat = lstatSync17(current);
|
|
22478
|
-
if (stat.isSymbolicLink() || current !== destination && !stat.isDirectory()) throw new Error("Scaffolding refuses linked or invalid output paths: " + filename2);
|
|
22479
|
-
if (relative10(canonicalRoot, realpathSync8(current)).startsWith("..")) throw new Error("Scaffold output must remain inside its directory");
|
|
22480
|
-
if (current === destination && (!stat.isFile() || readFileSync20(current, "utf8") !== content)) throw new Error("Scaffolding refuses to overwrite existing content: " + filename2);
|
|
22481
|
-
} catch (error) {
|
|
22482
|
-
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
|
|
22483
|
-
if (current === destination) pending.push(filename2);
|
|
22484
|
-
}
|
|
22485
|
-
current = dirname13(current);
|
|
22486
|
-
}
|
|
22487
|
-
}
|
|
22488
|
-
for (const filename2 of pending) {
|
|
22489
|
-
const destination = join13(root, filename2);
|
|
22490
|
-
mkdirSync8(dirname13(destination), { recursive: true });
|
|
22491
|
-
writeFileSync10(destination, files[filename2], { flag: "wx" });
|
|
22492
|
-
}
|
|
22493
|
-
return { template: "react-vite-hono", created: pending, unchanged: Object.keys(files).length - pending.length, installed: false, deployed: false };
|
|
22494
|
-
}
|
|
22495
|
-
|
|
22496
|
-
// src/local-inspection-command.ts
|
|
22497
|
-
function localInspectionCommand(args2) {
|
|
22498
|
-
const routes = { tables: "/v1/database/tables", rows: "/v1/database/rows", buckets: "/v1/storage/buckets", objects: "/v1/storage/objects", preview: "/v1/storage/preview", events: "/v1/events" };
|
|
22499
|
-
const kind = args2[0];
|
|
22500
|
-
if (!Object.hasOwn(routes, kind)) throw new Error("Use backify inspect tables|rows|buckets|objects|preview|events --local [--json]");
|
|
22501
|
-
const allowed = kind === "rows" ? ["--table", "--limit", "--offset"] : kind === "objects" ? ["--bucket", "--prefix", "--limit", "--offset"] : kind === "preview" ? ["--bucket", "--key"] : [];
|
|
22502
|
-
const values = /* @__PURE__ */ new Map();
|
|
22503
|
-
const flags = /* @__PURE__ */ new Set();
|
|
22504
|
-
for (let index = 1; index < args2.length; index++) {
|
|
22505
|
-
const name2 = args2[index];
|
|
22506
|
-
if (["--local", "--json"].includes(name2)) {
|
|
22507
|
-
if (flags.has(name2)) throw new Error("Duplicate inspection flag");
|
|
22508
|
-
flags.add(name2);
|
|
22509
|
-
continue;
|
|
22510
|
-
}
|
|
22511
|
-
if (!allowed.includes(name2) || values.has(name2) || args2[index + 1] === void 0 || args2[index + 1].startsWith("--")) throw new Error("Invalid or missing inspection option");
|
|
22512
|
-
values.set(name2, args2[++index]);
|
|
22513
|
-
}
|
|
22514
|
-
if (!flags.has("--local")) throw new Error("Inspection requires --local; no cloud or production data is queried");
|
|
22515
|
-
const required = (name2) => {
|
|
22516
|
-
const value = values.get(name2);
|
|
22517
|
-
if (!value) throw new Error("Required inspection option: " + name2);
|
|
22518
|
-
return value;
|
|
22519
|
-
};
|
|
22520
|
-
const page = () => {
|
|
22521
|
-
const number = (name2, fallback, minimum, maximum) => {
|
|
22522
|
-
const value = values.get(name2);
|
|
22523
|
-
if (value !== void 0 && !/^\d+$/.test(value)) throw new Error("Invalid inspection page");
|
|
22524
|
-
const parsed = value === void 0 ? fallback : Number(value);
|
|
22525
|
-
if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) throw new Error("Invalid inspection page");
|
|
22526
|
-
return parsed;
|
|
22527
|
-
};
|
|
22528
|
-
return { limit: number("--limit", 25, 1, 50), offset: number("--offset", 0, 0, 1e4) };
|
|
22529
|
-
};
|
|
22530
|
-
const input = kind === "rows" ? { table: required("--table"), ...page() } : kind === "objects" ? { bucket: required("--bucket"), ...page(), ...values.has("--prefix") ? { prefix: values.get("--prefix") } : {} } : kind === "preview" ? { bucket: required("--bucket"), key: required("--key") } : void 0;
|
|
22531
|
-
return { kind, path: routes[kind], input };
|
|
22532
|
-
}
|
|
22533
|
-
|
|
22534
|
-
// src/development.ts
|
|
22535
|
-
import { existsSync as existsSync14, readFileSync as readFileSync23, readdirSync as readdirSync8, writeFileSync as writeFileSync13, renameSync as renameSync4, rmSync as rmSync2, mkdtempSync as mkdtempSync2, lstatSync as lstatSync19, cpSync } from "node:fs";
|
|
22536
|
-
import { dirname as dirname16, join as join16, resolve as resolve23 } from "node:path";
|
|
22537
|
-
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
22538
|
-
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
22539
|
-
|
|
22540
|
-
// src/setup.ts
|
|
22541
|
-
import { existsSync as existsSync13, readFileSync as readFileSync22, writeFileSync as writeFileSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync7, lstatSync as lstatSync18 } from "node:fs";
|
|
22542
|
-
import { basename as basename3, dirname as dirname15, join as join15, resolve as resolve22, isAbsolute as isAbsolute16 } from "node:path";
|
|
22543
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
22544
|
-
|
|
22545
|
-
// src/delivery.ts
|
|
22546
|
-
import { existsSync as existsSync12, readFileSync as readFileSync21, writeFileSync as writeFileSync11 } from "node:fs";
|
|
22547
|
-
import { dirname as dirname14, join as join14, relative as relativePath } from "node:path";
|
|
22548
|
-
import { createHash as createHash15 } from "node:crypto";
|
|
22549
|
-
var previousGeneratedWorkflowHashes = /* @__PURE__ */ new Set(["33e962274e8493f7cbac2afbac05a4e96afeec49ee08cf2bf82f8add9081b28b", "578dfb4c771c623fd04454e8a604efd701d5563d11e11191324862369bd24718", "6eb280ae7bdb14c390011686c41400f59c023304c3015d8440b7e80aaafa1c11", "545e03a1d5b996ade179e0e60ed5c86accdadc1982c1c25efd094e34f72f47ee"]);
|
|
22550
|
-
function validateDeliveryInstructions(root) {
|
|
22551
|
-
for (const filename2 of ["BACKIFY.md", "BACKIFY-WORKFLOW.md"]) {
|
|
22552
|
-
const path = join14(root, filename2);
|
|
22553
|
-
if (existsSync12(path)) {
|
|
22554
|
-
const existing = readFileSync21(path, "utf8").replace(/\r\n/g, "\n");
|
|
22555
|
-
if (existing !== deliveryInstructions && !previousGeneratedWorkflowHashes.has(createHash15("sha256").update(existing).digest("hex"))) throw new Error(filename2 + " contains different instructions. Review it before initializing; it will not be overwritten.");
|
|
22556
|
-
}
|
|
22557
|
-
}
|
|
22558
|
-
}
|
|
22559
|
-
function installDeliveryInstructions(root, instructionFiles2) {
|
|
22560
|
-
validateDeliveryInstructions(root);
|
|
22561
|
-
writeFileSync11(join14(root, "BACKIFY.md"), deliveryInstructions);
|
|
22562
|
-
for (const relative11 of instructionFiles2) {
|
|
22563
|
-
const instructionPath = join14(root, relative11), content = readFileSync21(instructionPath, "utf8");
|
|
22564
|
-
const link = relativePath(dirname14(instructionPath), join14(root, "BACKIFY.md")).replaceAll("\\", "/");
|
|
22565
|
-
const legacyLink = relativePath(dirname14(instructionPath), join14(root, "BACKIFY-WORKFLOW.md")).replaceAll("\\", "/");
|
|
22566
|
-
const legacy = "For application delivery and recording each user request, follow [BACKIFY-WORKFLOW.md](" + legacyLink + ").";
|
|
22567
|
-
const guidance = "For application delivery and recording each user request, follow [BACKIFY.md](" + link + ").";
|
|
22568
|
-
if (content.includes(legacy)) writeFileSync11(instructionPath, content.replace(legacy, guidance));
|
|
22569
|
-
else if (!content.includes("## Backify delivery")) writeFileSync11(instructionPath, content.trimEnd() + "\n\n## Backify delivery\n\n" + guidance + "\n");
|
|
22570
|
-
}
|
|
22571
|
-
}
|
|
22572
|
-
|
|
22573
|
-
// src/setup.ts
|
|
22574
|
-
var instructionFiles = ["AGENTS.md"];
|
|
22575
|
-
var agentInstructionFiles = { codex: "AGENTS.md", claude: "CLAUDE.md", copilot: join15(".github", "copilot-instructions.md"), gemini: "GEMINI.md" };
|
|
22576
|
-
function instructionSelection(root, agent, instructionFile) {
|
|
22577
|
-
const configPath2 = join15(root, "backify.json"), configured = existsSync13(configPath2) ? JSON.parse(readFileSync22(configPath2, "utf8")) : {};
|
|
22578
|
-
const custom = instructionFile ?? (agent === void 0 ? configured.instructionFile : void 0);
|
|
22579
|
-
const selected = instructionFile !== void 0 ? "custom" : agent ?? configured.agent ?? "codex";
|
|
22580
|
-
if (typeof selected !== "string" || selected !== "custom" && !Object.hasOwn(agentInstructionFiles, selected)) throw new Error("Select the current tool with --agent or --instruction-file <relative-path>.");
|
|
22581
|
-
const filename2 = custom ?? agentInstructionFiles[selected];
|
|
22582
|
-
if (typeof filename2 !== "string" || !filename2.trim()) throw new Error("Select the current tool with --agent codex, claude, copilot, or gemini, or --instruction-file <relative-path> for any other tool.");
|
|
22583
|
-
const normalized = filename2.replaceAll("\\", "/"), parts = normalized.split("/");
|
|
22584
|
-
if (isAbsolute16(filename2) || /^[A-Za-z]:/.test(filename2) || parts.some((part) => !part || part === "." || part === ".." || part.toLowerCase() === ".git") || ["backify.md", "backify-workflow.md", "backify.json", "package.json", "package-lock.json"].includes(normalized.toLowerCase())) throw new Error("Instruction file must be a relative tool-instruction path inside the project, not Git metadata or a Backify configuration file.");
|
|
22585
|
-
for (let index = 1; index <= parts.length; index++) {
|
|
22586
|
-
const entry = join15(root, ...parts.slice(0, index));
|
|
22587
|
-
try {
|
|
22588
|
-
const metadata = lstatSync18(entry);
|
|
22589
|
-
if (metadata.isSymbolicLink() || index < parts.length && !metadata.isDirectory() || index === parts.length && !metadata.isFile()) throw new Error("Instruction file path cannot traverse symlinks or replace a directory.");
|
|
22590
|
-
} catch (reason) {
|
|
22591
|
-
if (reason.code !== "ENOENT") throw reason;
|
|
22592
|
-
}
|
|
22593
|
-
}
|
|
22594
|
-
return { agent: selected, instructionFile: normalized, files: [normalized] };
|
|
22595
|
-
}
|
|
22596
|
-
function selectedInstructionFiles(root = process.cwd(), agent, instructionFile) {
|
|
22597
|
-
return instructionSelection(root, agent, instructionFile).files;
|
|
22598
|
-
}
|
|
22599
|
-
function isToolWorkspaceEntry(entry, root = process.cwd()) {
|
|
22600
|
-
if (entry.name.toLowerCase() === ".git" || entry.name.toLowerCase() === ".env" || entry.name.toLowerCase().startsWith(".env.")) return false;
|
|
22601
|
-
return entry.isDirectory() && (entry.name.startsWith(".") || ["work", "outputs"].includes(entry.name) || readdirSync7(join15(root, entry.name)).length === 0) || entry.isFile() && (entry.name.startsWith(".") || /\.(md|mdc)$/i.test(entry.name));
|
|
22602
|
-
}
|
|
22603
|
-
function detectPackageManager(root = process.cwd()) {
|
|
22604
|
-
for (const [file, name2] of [["pnpm-lock.yaml", "pnpm"], ["yarn.lock", "yarn"], ["bun.lockb", "bun"], ["bun.lock", "bun"], ["package-lock.json", "npm"]]) if (existsSync13(join15(root, file))) return name2;
|
|
22605
|
-
return "npm";
|
|
22606
|
-
}
|
|
22607
|
-
function updateInstructions(root = process.cwd(), agent, instructionFile) {
|
|
22608
|
-
const updates = selectedInstructionFiles(root, agent, instructionFile).map((relative11) => {
|
|
22609
|
-
const path = join15(root, relative11);
|
|
22610
|
-
return { path, content: updateManagedInstructions(existsSync13(path) ? readFileSync22(path, "utf8") : "") };
|
|
22611
|
-
});
|
|
22612
|
-
for (const { path, content } of updates) {
|
|
22613
|
-
mkdirSync9(dirname15(path), { recursive: true });
|
|
22614
|
-
writeFileSync12(path, content);
|
|
22615
|
-
}
|
|
22616
|
-
}
|
|
22617
|
-
function synchronize(root = process.cwd(), agent, instructionFile) {
|
|
22618
|
-
const path = join15(root, "backify.json");
|
|
22619
|
-
if (!existsSync13(path)) throw new Error("backify.json is missing; run backify init --app app_xxxxx");
|
|
22620
|
-
const value = JSON.parse(readFileSync22(path, "utf8")), issues = validateConfig(value);
|
|
22621
|
-
if (issues.length) throw new Error(issues.join("; "));
|
|
22622
|
-
if (value.instructionVersion > INSTRUCTION_VERSION) throw new Error("This app uses newer instructions. Install a matching or newer CLI before syncing.");
|
|
22623
|
-
const selected = instructionSelection(root, agent, instructionFile);
|
|
22624
|
-
validateDeliveryInstructions(root);
|
|
22625
|
-
updateInstructions(root, selected.agent, selected.instructionFile);
|
|
22626
|
-
installDeliveryInstructions(root, selected.files);
|
|
22627
|
-
value.agent = selected.agent;
|
|
22628
|
-
value.instructionFile = selected.instructionFile;
|
|
22629
|
-
value.instructionVersion = INSTRUCTION_VERSION;
|
|
22630
|
-
writeFileSync12(path, JSON.stringify(value, null, 2) + "\n");
|
|
22631
|
-
}
|
|
22632
|
-
function sdkInstalled(root = process.cwd()) {
|
|
22633
|
-
try {
|
|
22634
|
-
const pkg = JSON.parse(readFileSync22(join15(root, "package.json"), "utf8"));
|
|
22635
|
-
return Boolean(pkg.dependencies?.[SDK_PACKAGE] || pkg.devDependencies?.[SDK_PACKAGE]);
|
|
22636
|
-
} catch {
|
|
22637
|
-
return false;
|
|
22638
|
-
}
|
|
22639
|
-
}
|
|
22640
|
-
function installSdk(root = process.cwd()) {
|
|
22641
|
-
if (sdkInstalled(root)) return;
|
|
22642
|
-
const manager = detectPackageManager(root), args2 = manager === "npm" ? ["install", SDK_PACKAGE] : ["add", SDK_PACKAGE], result = spawnSync2(manager, args2, { cwd: root, stdio: "inherit", shell: process.platform === "win32" });
|
|
22643
|
-
if (result.status !== 0) throw new Error(`Could not install ${SDK_PACKAGE} with ${manager}`);
|
|
22644
|
-
}
|
|
22645
|
-
function ensurePackageManifest(root) {
|
|
22646
|
-
const path = join15(root, "package.json");
|
|
22647
|
-
if (existsSync13(path)) return;
|
|
22648
|
-
const setupFiles = /* @__PURE__ */ new Set([".git", ".gitignore", ".github", "AGENTS.md", "CLAUDE.md", "GEMINI.md", "BACKIFY.md", "BACKIFY-WORKFLOW.md", "README.md", "LICENSE", "backify.json"]);
|
|
22649
|
-
if (readdirSync7(root, { withFileTypes: true }).some((entry) => !setupFiles.has(entry.name) && !isToolWorkspaceEntry(entry, root))) throw new Error("package.json is missing. Run backify init from the existing application's JavaScript/TypeScript package directory, or start in an empty folder. See https://agent.backify.dev/");
|
|
22650
|
-
const name2 = basename3(resolve22(root)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "backify-app";
|
|
22651
|
-
writeFileSync12(path, JSON.stringify({ name: name2, version: "0.0.0", private: true, type: "module" }, null, 2) + "\n", { flag: "wx" });
|
|
22652
|
-
}
|
|
22653
|
-
function initialize(appId, root = process.cwd(), install = true, agent, instructionFile) {
|
|
22654
|
-
if (!/^app_[A-Za-z0-9_-]+$/.test(appId)) throw new Error("Provide an application ID using --app app_xxxxx");
|
|
22655
|
-
const selected = instructionSelection(root, agent, instructionFile), files = selected.files;
|
|
22656
|
-
validateDeliveryInstructions(root);
|
|
22657
|
-
for (const relative11 of files) if (existsSync13(join15(root, relative11))) updateManagedInstructions(readFileSync22(join15(root, relative11), "utf8"));
|
|
22658
|
-
ensurePackageManifest(root);
|
|
22659
|
-
if (install) installSdk(root);
|
|
22660
|
-
const configPath2 = join15(root, "backify.json"), existing = existsSync13(configPath2) ? JSON.parse(readFileSync22(configPath2, "utf8")) : {};
|
|
22661
|
-
writeFileSync12(configPath2, JSON.stringify({ ...existing, ...createConfig(appId), agent: selected.agent, instructionFile: selected.instructionFile }, null, 2) + "\n");
|
|
22662
|
-
updateInstructions(root, selected.agent, selected.instructionFile);
|
|
22663
|
-
installDeliveryInstructions(root, files);
|
|
22664
|
-
}
|
|
22665
|
-
function doctor(root = process.cwd(), agent, instructionFile) {
|
|
22666
|
-
const issues = [], warnings = [];
|
|
22667
|
-
if (!sdkInstalled(root)) issues.push(`${SDK_PACKAGE} is not installed`);
|
|
22668
|
-
let config;
|
|
22669
|
-
try {
|
|
22670
|
-
config = JSON.parse(readFileSync22(join15(root, "backify.json"), "utf8"));
|
|
22671
|
-
issues.push(...validateConfig(config));
|
|
22672
|
-
} catch {
|
|
22673
|
-
issues.push("backify.json is missing or invalid JSON");
|
|
22674
|
-
}
|
|
22675
|
-
if (config && config.instructionVersion !== INSTRUCTION_VERSION) issues.push(`Repository instruction version ${config.instructionVersion}; CLI version ${INSTRUCTION_VERSION}. Use the matching CLI or run backify sync to upgrade.`);
|
|
22676
|
-
let files = instructionFiles;
|
|
22677
|
-
try {
|
|
22678
|
-
files = selectedInstructionFiles(root, agent, instructionFile);
|
|
22679
|
-
} catch (reason) {
|
|
22680
|
-
issues.push(reason instanceof Error ? reason.message : String(reason));
|
|
22681
|
-
}
|
|
22682
|
-
for (const relative11 of files) {
|
|
22683
|
-
const path = join15(root, relative11);
|
|
22684
|
-
if (!existsSync13(path)) {
|
|
22685
|
-
issues.push(`${relative11} is missing`);
|
|
22686
|
-
continue;
|
|
22687
|
-
}
|
|
22688
|
-
issues.push(...managedInstructionIssues(readFileSync22(path, "utf8")).map((issue) => `${relative11} ${issue}`));
|
|
22689
|
-
}
|
|
22690
|
-
let version = "unknown";
|
|
22691
|
-
try {
|
|
22692
|
-
const pkg = JSON.parse(readFileSync22(join15(root, "package.json"), "utf8"));
|
|
22693
|
-
version = pkg.dependencies?.[SDK_PACKAGE] ?? pkg.devDependencies?.[SDK_PACKAGE] ?? "missing";
|
|
22694
|
-
} catch {
|
|
22695
|
-
}
|
|
22696
|
-
return { valid: issues.length === 0, appId: config?.appId, sdkVersion: version, instructionVersion: config?.instructionVersion, currentInstructionVersion: INSTRUCTION_VERSION, issues, warnings };
|
|
22697
|
-
}
|
|
22698
|
-
|
|
22699
|
-
// src/development.ts
|
|
22700
|
-
function promptMetadata(args2) {
|
|
22701
|
-
const fields5 = [["--model", "model", 200], ["--ai-tool", "aiTool", 200], ["--access-level", "accessLevel", 500]];
|
|
22702
|
-
const metadata = {};
|
|
22703
|
-
for (const [flag, field, maxLength] of fields5) {
|
|
22704
|
-
const index = args2.indexOf(flag);
|
|
22705
|
-
if (index < 0) continue;
|
|
22706
|
-
const value = args2[index + 1]?.trim();
|
|
22707
|
-
if (!value || value.startsWith("--") || value.length > maxLength || /[\u0000-\u001f\u007f-\u009f]|bfcli_[0-9a-f]{64}|--pair\s+[0-9a-f]{64}/i.test(value))
|
|
22708
|
-
throw new Error(`Provide ${flag} with a nonempty value of at most ${maxLength} characters without control characters or credentials.`);
|
|
22709
|
-
metadata[field] = value;
|
|
22710
|
-
}
|
|
22711
|
-
return metadata;
|
|
22712
|
-
}
|
|
22713
|
-
function applicationGuid(appId) {
|
|
22714
|
-
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(appId)) return appId.toLowerCase();
|
|
22715
|
-
const match = /^app_([0-9a-f]{32})$/i.exec(appId);
|
|
22716
|
-
if (!match) throw new Error("Use the application ID supplied by Backify.");
|
|
22717
|
-
const value = match[1];
|
|
22718
|
-
return [value.slice(0, 8), value.slice(8, 12), value.slice(12, 16), value.slice(16, 20), value.slice(20)].join("-");
|
|
22719
|
-
}
|
|
22720
|
-
function projectApplication(root = process.cwd()) {
|
|
22721
|
-
if (!existsSync14(join16(root, "backify.json"))) throw new Error("Run backify connect --app <application-id>, then backify init.");
|
|
22722
|
-
return JSON.parse(readFileSync23(join16(root, "backify.json"), "utf8")).appId;
|
|
22723
|
-
}
|
|
22724
|
-
function apiAddress(value) {
|
|
22725
|
-
const address = new URL(value);
|
|
22726
|
-
if (address.username || address.password || address.search || address.hash || address.protocol !== "https:" && !(address.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(address.hostname))) throw new Error("Backify API must use HTTPS (HTTP is allowed only on loopback for local development).");
|
|
22727
|
-
return address.toString().replace(/\/$/, "");
|
|
23178
|
+
provider-readiness claim, cloud deployment or browser authentication is implied.
|
|
23179
|
+
`
|
|
23180
|
+
};
|
|
22728
23181
|
}
|
|
22729
|
-
function
|
|
22730
|
-
const
|
|
22731
|
-
|
|
22732
|
-
|
|
22733
|
-
|
|
22734
|
-
|
|
22735
|
-
|
|
22736
|
-
|
|
23182
|
+
function scaffoldFullStack(options) {
|
|
23183
|
+
const files = fullStackTemplate(options.appId, options.name);
|
|
23184
|
+
const root = resolve23(options.root ?? process.cwd());
|
|
23185
|
+
if (!existsSync14(root) || !lstatSync19(root).isDirectory() || lstatSync19(root).isSymbolicLink()) throw new Error("Scaffold destination must be an existing non-linked directory");
|
|
23186
|
+
const allowed = /* @__PURE__ */ new Set([...Object.keys(files).map((filename2) => filename2.split("/")[0]), ".git", "node_modules", "package-lock.json", ".backify"]);
|
|
23187
|
+
if (readdirSync8(root).some((filename2) => !allowed.has(filename2))) throw new Error("Scaffolding requires an empty folder or an unchanged generated starter");
|
|
23188
|
+
const canonicalRoot = realpathSync8(root);
|
|
23189
|
+
const pending = [];
|
|
23190
|
+
for (const [filename2, content] of Object.entries(files)) {
|
|
23191
|
+
const destination = join16(root, filename2);
|
|
23192
|
+
let current = destination;
|
|
23193
|
+
while (current !== root) {
|
|
22737
23194
|
try {
|
|
22738
|
-
const
|
|
22739
|
-
|
|
22740
|
-
|
|
23195
|
+
const stat = lstatSync19(current);
|
|
23196
|
+
if (stat.isSymbolicLink() || current !== destination && !stat.isDirectory()) throw new Error("Scaffolding refuses linked or invalid output paths: " + filename2);
|
|
23197
|
+
if (relative10(canonicalRoot, realpathSync8(current)).startsWith("..")) throw new Error("Scaffold output must remain inside its directory");
|
|
23198
|
+
if (current === destination && (!stat.isFile() || readFileSync23(current, "utf8") !== content)) throw new Error("Scaffolding refuses to overwrite existing content: " + filename2);
|
|
23199
|
+
} catch (error) {
|
|
23200
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
|
|
23201
|
+
if (current === destination) pending.push(filename2);
|
|
22741
23202
|
}
|
|
22742
|
-
|
|
22743
|
-
}
|
|
22744
|
-
return response.status === 204 ? void 0 : await response.json();
|
|
22745
|
-
};
|
|
22746
|
-
}
|
|
22747
|
-
function saveUserConfig(path, config) {
|
|
22748
|
-
const temporary = path + "." + randomUUID9() + ".tmp";
|
|
22749
|
-
try {
|
|
22750
|
-
writeFileSync13(temporary, JSON.stringify(config, null, 2) + "\n", { mode: 384, flag: "wx" });
|
|
22751
|
-
if (process.platform === "win32") {
|
|
22752
|
-
const account = [process.env.USERDOMAIN, process.env.USERNAME].filter(Boolean).join("\\");
|
|
22753
|
-
if (!account) throw new Error("Cannot identify the account to protect CLI credentials.");
|
|
22754
|
-
const secured = spawnSync3("icacls", [temporary, "/inheritance:r", "/grant:r", account + ":(F)"], { windowsHide: true, encoding: "utf8" });
|
|
22755
|
-
if (secured.status !== 0) throw new Error("Could not restrict CLI credential file permissions.");
|
|
22756
|
-
}
|
|
22757
|
-
renameSync4(temporary, path);
|
|
22758
|
-
} finally {
|
|
22759
|
-
rmSync2(temporary, { force: true });
|
|
22760
|
-
}
|
|
22761
|
-
}
|
|
22762
|
-
function repositoryName(remote) {
|
|
22763
|
-
const match = /^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?\/?$/i.exec(remote);
|
|
22764
|
-
if (!match) throw new Error("Repository must be a credential-free GitHub URL.");
|
|
22765
|
-
return match[1].toLowerCase();
|
|
22766
|
-
}
|
|
22767
|
-
function git2(args2, root = process.cwd(), environment = process.env) {
|
|
22768
|
-
const result = spawnSync3("git", args2, { cwd: root, env: environment, encoding: "utf8", windowsHide: true, maxBuffer: 2 * 1024 * 1024 });
|
|
22769
|
-
if (result.status !== 0) throw new Error("Git " + args2[0] + " failed. Check repository access, branch protection, and local changes.");
|
|
22770
|
-
return result.stdout.trim();
|
|
22771
|
-
}
|
|
22772
|
-
function gitEnvironment(repository, token) {
|
|
22773
|
-
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) throw new Error("Invalid repository.");
|
|
22774
|
-
return { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_COUNT: "3", GIT_CONFIG_KEY_0: "http.https://github.com/" + repository + ".git.extraheader", GIT_CONFIG_VALUE_0: "Authorization: Basic " + Buffer.from("x-access-token:" + token).toString("base64"), GIT_CONFIG_KEY_1: "credential.helper", GIT_CONFIG_VALUE_1: "", GIT_CONFIG_KEY_2: "http.followRedirects", GIT_CONFIG_VALUE_2: "false" };
|
|
22775
|
-
}
|
|
22776
|
-
function validateCloneMerge(source, target) {
|
|
22777
|
-
for (const entry of readdirSync8(source, { withFileTypes: true })) {
|
|
22778
|
-
const destination = join16(target, entry.name);
|
|
22779
|
-
let existing;
|
|
22780
|
-
try {
|
|
22781
|
-
existing = lstatSync19(destination);
|
|
22782
|
-
} catch (reason) {
|
|
22783
|
-
if (reason.code === "ENOENT") continue;
|
|
22784
|
-
throw reason;
|
|
23203
|
+
current = dirname16(current);
|
|
22785
23204
|
}
|
|
22786
|
-
if (entry.isDirectory() && existing.isDirectory() && !existing.isSymbolicLink()) validateCloneMerge(join16(source, entry.name), destination);
|
|
22787
|
-
else throw new Error("Clone conflicts with existing workspace entry: " + destination + ". Existing files are never overwritten; reconcile the conflict or use a new folder.");
|
|
22788
|
-
}
|
|
22789
|
-
}
|
|
22790
|
-
async function cloneProject(call2, appId, root = process.cwd()) {
|
|
22791
|
-
const status = await call2("/apps/" + applicationGuid(appId) + "/developer");
|
|
22792
|
-
if (!status.repository || !status.branch || !status.workflowReady) throw new Error("Connect the repository in Backify and wait for workflow setup.");
|
|
22793
|
-
if (readdirSync8(root, { withFileTypes: true }).some((entry) => !isToolWorkspaceEntry(entry, root))) throw new Error("--clone requires an empty directory or a workspace containing only AI tool metadata and default work/outputs directories. Existing application files and Git repositories are never overwritten.");
|
|
22794
|
-
const credential = await call2("/apps/" + applicationGuid(appId) + "/developer/git-credential", { method: "POST" });
|
|
22795
|
-
if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed. Run connect again.");
|
|
22796
|
-
const staging = mkdtempSync2(join16(dirname16(resolve23(root)), ".backify-clone-"));
|
|
22797
|
-
try {
|
|
22798
|
-
git2(["clone", "--single-branch", "--branch", status.branch, "--", "https://github.com/" + status.repository + ".git", "."], staging, gitEnvironment(status.repository, credential.token));
|
|
22799
|
-
validateCloneMerge(staging, root);
|
|
22800
|
-
for (const entry of readdirSync8(staging)) cpSync(join16(staging, entry), join16(root, entry), { recursive: true, force: false, errorOnExist: true, dereference: false });
|
|
22801
|
-
} finally {
|
|
22802
|
-
if (dirname16(staging) !== dirname16(resolve23(root)) || !staging.startsWith(join16(dirname16(resolve23(root)), ".backify-clone-"))) throw new Error("Refusing to clean up an unexpected clone staging path.");
|
|
22803
|
-
rmSync2(staging, { recursive: true, force: true });
|
|
22804
|
-
}
|
|
22805
|
-
}
|
|
22806
|
-
async function pullProject(call2, appId, root = process.cwd()) {
|
|
22807
|
-
if (existsSync14(join16(root, "backify.json")) && applicationGuid(projectApplication(root)) !== applicationGuid(appId)) throw new Error("Local application ID does not match the connected application.");
|
|
22808
|
-
const prefix = "/apps/" + applicationGuid(appId);
|
|
22809
|
-
const status = await call2(prefix + "/developer");
|
|
22810
|
-
if (!status.repository || !status.branch) throw new Error("Connect the repository and branch in Backify before pulling.");
|
|
22811
|
-
if (resolve23(git2(["rev-parse", "--show-toplevel"], root)) !== resolve23(root)) throw new Error("Run pull from the connected repository root.");
|
|
22812
|
-
if (repositoryName(git2(["remote", "get-url", "origin"], root)) !== status.repository.toLowerCase()) throw new Error("Local origin does not match the repository connected in Backify.");
|
|
22813
|
-
if (git2(["branch", "--show-current"], root) !== status.branch) throw new Error("Local branch does not match the branch connected in Backify. The CLI never switches branches automatically.");
|
|
22814
|
-
git2(["check-ref-format", "--branch", status.branch], root);
|
|
22815
|
-
const credential = await call2(prefix + "/developer/git-credential", { method: "POST" });
|
|
22816
|
-
if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed; reconnect before pulling.");
|
|
22817
|
-
git2(["fetch", "--no-tags", "--", "https://github.com/" + status.repository + ".git", "refs/heads/" + status.branch + ":refs/remotes/origin/" + status.branch], root, gitEnvironment(status.repository, credential.token));
|
|
22818
|
-
const remoteCommit = git2(["rev-parse", "FETCH_HEAD"], root);
|
|
22819
|
-
const counts = git2(["rev-list", "--left-right", "--count", "HEAD..." + remoteCommit], root).split(/\s+/).map(Number);
|
|
22820
|
-
if (counts.length !== 2 || counts.some((count) => !Number.isSafeInteger(count) || count < 0)) throw new Error("Could not compare local and remote history.");
|
|
22821
|
-
const [ahead, behind] = counts;
|
|
22822
|
-
if (!behind) return { status: "up-to-date", ahead, behind, commit: git2(["rev-parse", "HEAD"], root) };
|
|
22823
|
-
if (ahead) return { status: "blocked", reason: "Local and GitHub history have diverged. Review and reconcile them explicitly; no rebase, reset, or force-push was performed.", ahead, behind };
|
|
22824
|
-
if (git2(["status", "--porcelain"], root)) return { status: "blocked", reason: "Local changes or untracked files are present. Commit, ignore, or otherwise resolve them explicitly before updating; nothing was stashed or overwritten.", ahead, behind };
|
|
22825
|
-
git2(["merge", "--ff-only", "--", remoteCommit], root);
|
|
22826
|
-
return { status: "updated", ahead: 0, behind: 0, commit: git2(["rev-parse", "HEAD"], root) };
|
|
22827
|
-
}
|
|
22828
|
-
async function readPrompt(input) {
|
|
22829
|
-
const chunks = [];
|
|
22830
|
-
let size = 0;
|
|
22831
|
-
for await (const chunk of input) {
|
|
22832
|
-
const bytes = Buffer.from(chunk);
|
|
22833
|
-
size += bytes.length;
|
|
22834
|
-
if (size > 128e3) throw new Error("Prompt exceeds the size limit.");
|
|
22835
|
-
chunks.push(bytes);
|
|
22836
|
-
}
|
|
22837
|
-
const text3 = Buffer.concat(chunks).toString("utf8");
|
|
22838
|
-
if (!text3.trim() || text3.length > 32e3) throw new Error("Provide between 1 and 32000 characters of user prompt text.");
|
|
22839
|
-
if (/\bbfcli_[0-9a-f]{64}\b|--pair\s+[0-9a-f]{64}/i.test(text3)) throw new Error("Do not record setup prompts or Backify credentials.");
|
|
22840
|
-
return text3;
|
|
22841
|
-
}
|
|
22842
|
-
function attachmentNames(values) {
|
|
22843
|
-
if (values.length > 50) throw new Error("At most 50 attachment filenames are allowed.");
|
|
22844
|
-
return [...new Set(values.map((value) => {
|
|
22845
|
-
const filename2 = value.replace(/\\/g, "/").split("/").pop();
|
|
22846
|
-
if (!filename2 || filename2 === "." || filename2 === ".." || filename2.length > 255 || /[\x00-\x1f\x7f]/.test(filename2)) throw new Error("Invalid attachment filename.");
|
|
22847
|
-
return filename2;
|
|
22848
|
-
}))];
|
|
22849
|
-
}
|
|
22850
|
-
function localReport(runChecks, root = process.cwd()) {
|
|
22851
|
-
const pkg = JSON.parse(readFileSync23(join16(root, "package.json"), "utf8"));
|
|
22852
|
-
const dependencies = { ...pkg.devDependencies, ...pkg.dependencies };
|
|
22853
|
-
const framework = ["next", "vite", "astro", "@sveltejs/kit", "nuxt", "react", "express"].find((name2) => dependencies[name2]) ?? "unknown";
|
|
22854
|
-
const setup = doctor(root);
|
|
22855
|
-
const checks = [{ name: "backify doctor", status: setup.valid ? "passed" : "failed" }];
|
|
22856
|
-
const manager = detectPackageManager(root);
|
|
22857
|
-
for (const name2 of ["typecheck", "lint", "test", "build"]) {
|
|
22858
|
-
if (!pkg.scripts?.[name2]) continue;
|
|
22859
|
-
const result = runChecks ? spawnSync3(manager, ["run", name2], { cwd: root, stdio: ["ignore", "inherit", "inherit"], env: { ...process.env, CI: "true" }, shell: process.platform === "win32", windowsHide: true, timeout: 10 * 6e4 }) : void 0;
|
|
22860
|
-
checks.push({ name: name2, status: result ? result.status === 0 ? "passed" : "failed" : "not-run" });
|
|
22861
|
-
}
|
|
22862
|
-
let branch = null, commitSha = null, dirty = true;
|
|
22863
|
-
try {
|
|
22864
|
-
branch = git2(["branch", "--show-current"], root);
|
|
22865
|
-
commitSha = git2(["rev-parse", "HEAD"], root);
|
|
22866
|
-
dirty = Boolean(git2(["status", "--porcelain"], root));
|
|
22867
|
-
} catch {
|
|
22868
|
-
}
|
|
22869
|
-
const outputDirectory = ["dist", "build", "out", "."].find((directory2) => existsSync14(join16(root, directory2, "index.html"))) ?? null;
|
|
22870
|
-
return { framework, sdkVersion: dependencies["@backifydev/sdk"] ?? null, branch, commitSha, dirty, buildCommand: pkg.scripts?.build ? manager + " run build" : null, outputDirectory, hasDockerfile: existsSync14(join16(root, "Dockerfile")), checks };
|
|
22871
|
-
}
|
|
22872
|
-
function matchesSource(release2, commit) {
|
|
22873
|
-
return release2?.commitSha === commit || release2?.sourceCommitSha === commit;
|
|
22874
|
-
}
|
|
22875
|
-
function pushSource(appId, commit, branch, destination, root, environment) {
|
|
22876
|
-
const remoteHead = git2(["ls-remote", "--", destination, "refs/heads/" + branch], root, environment).split(/\s+/)[0];
|
|
22877
|
-
if (remoteHead === commit) return true;
|
|
22878
|
-
if (remoteHead) {
|
|
22879
|
-
git2(["fetch", "--no-tags", "--", destination, "refs/heads/" + branch], root, environment);
|
|
22880
|
-
const fetchedHead = git2(["rev-parse", "FETCH_HEAD"], root);
|
|
22881
|
-
if (fetchedHead === commit) return true;
|
|
22882
|
-
const parent = git2(["show", "-s", "--format=%P", fetchedHead], root);
|
|
22883
|
-
const message = git2(["show", "-s", "--format=%B", fetchedHead], root);
|
|
22884
|
-
if (parent === commit && message === "Backify build " + applicationGuid(appId).replaceAll("-", "") + ": " + commit) return true;
|
|
22885
|
-
if (git2(["merge-base", commit, fetchedHead], root) !== fetchedHead) throw new Error("Remote history contains other changes. Run backify pull and reconcile them before shipping; nothing was pushed or overwritten.");
|
|
22886
23205
|
}
|
|
22887
|
-
|
|
22888
|
-
|
|
22889
|
-
}
|
|
22890
|
-
|
|
22891
|
-
const deadline = Date.now() + (options.timeoutMs ?? 20 * 6e4);
|
|
22892
|
-
const sleep = options.sleep ?? ((delay) => new Promise((resolve24) => setTimeout(resolve24, delay)));
|
|
22893
|
-
do {
|
|
22894
|
-
const deployments = await call2(prefix + "/deployments");
|
|
22895
|
-
const matching = deployments.find((release2) => release2.kind === "preview" && matchesSource(release2, commit) && !options.previousDeploymentIds?.includes(release2.id));
|
|
22896
|
-
if (matching?.status === "failed") throw new Error("Deployment of this source failed. Run backify status and inspect its deployment steps.");
|
|
22897
|
-
const overview = await call2(prefix + "/releases");
|
|
22898
|
-
if (matchesSource(overview.preview, commit) && overview.preview?.status === "succeeded" && overview.preview.healthResult === "healthy") return overview;
|
|
22899
|
-
const build3 = await call2(prefix + "/developer/build?commitSha=" + commit);
|
|
22900
|
-
if ((options.previousBuildRunId === void 0 || build3.runId !== options.previousBuildRunId) && ["failure", "cancelled", "timed_out", "action_required", "startup_failure"].includes(build3.conclusion ?? "")) throw new Error("GitHub build " + build3.conclusion + ". " + (build3.url ?? "Run backify status for details."));
|
|
22901
|
-
await sleep(options.intervalMs ?? 4e3);
|
|
22902
|
-
} while (Date.now() < deadline);
|
|
22903
|
-
throw new Error("Timed out waiting for this commit's healthy preview. Run backify status; no production release was requested.");
|
|
22904
|
-
}
|
|
22905
|
-
async function ship(call2, appId, options = {}) {
|
|
22906
|
-
const root = options.root ?? process.cwd(), prefix = "/apps/" + applicationGuid(appId);
|
|
22907
|
-
const status = await call2(prefix + "/developer");
|
|
22908
|
-
if (!status.repository || !status.branch || !status.workflowReady) throw new Error("The application's repository and build workflow must be connected in Backify.");
|
|
22909
|
-
if (!doctor(root).valid) throw new Error("Run backify doctor and fix project setup before shipping.");
|
|
22910
|
-
if (applicationGuid(projectApplication(root)) !== applicationGuid(appId)) throw new Error("Local application ID does not match the application being shipped.");
|
|
22911
|
-
const actualRoot = git2(["rev-parse", "--show-toplevel"], root);
|
|
22912
|
-
if (resolve23(actualRoot).toLowerCase() !== resolve23(root).toLowerCase()) throw new Error("Shipping currently requires the application at the repository root.");
|
|
22913
|
-
const origin = git2(["remote", "get-url", "origin"], root);
|
|
22914
|
-
const pushOrigin = git2(["remote", "get-url", "--push", "origin"], root);
|
|
22915
|
-
if (repositoryName(origin) !== status.repository.toLowerCase() || repositoryName(pushOrigin) !== status.repository.toLowerCase()) throw new Error("Local repository does not match this Backify application.");
|
|
22916
|
-
if (git2(["branch", "--show-current"], root) !== status.branch) throw new Error("Use the connected branch: " + status.branch);
|
|
22917
|
-
if (git2(["status", "--porcelain"], root)) throw new Error("Commit application changes before shipping. The CLI never stages or commits unrelated files.");
|
|
22918
|
-
const commit = git2(["rev-parse", "HEAD"], root);
|
|
22919
|
-
const report = localReport(true, root);
|
|
22920
|
-
await call2(prefix + "/developer/report", { method: "POST", body: JSON.stringify(report) });
|
|
22921
|
-
if (report.checks.some((check) => check.status === "failed")) throw new Error("Checks failed; nothing was pushed.");
|
|
22922
|
-
if (status.hostingTargetType === "backify-cloud" && !report.outputDirectory && !report.hasDockerfile) throw new Error("Backify Cloud requires a static build containing index.html or a Dockerfile serving port 8080.");
|
|
22923
|
-
if (status.hostingTargetType === "azure" && !report.hasDockerfile) throw new Error("Customer Azure requires a repository-root Dockerfile.");
|
|
22924
|
-
if (git2(["status", "--porcelain"], root) || git2(["rev-parse", "HEAD"], root) !== commit) throw new Error("Checks changed tracked source. Review and commit those changes before shipping.");
|
|
22925
|
-
const previousDeployments = await call2(prefix + "/deployments");
|
|
22926
|
-
const previousBuild = await call2(prefix + "/developer/build?commitSha=" + commit);
|
|
22927
|
-
const credential = await call2(prefix + "/developer/git-credential", { method: "POST" });
|
|
22928
|
-
if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed; reconnect before shipping.");
|
|
22929
|
-
const environment = gitEnvironment(status.repository, credential.token);
|
|
22930
|
-
const destination = "https://github.com/" + status.repository + ".git";
|
|
22931
|
-
const alreadyPushed = pushSource(appId, commit, status.branch, destination, root, environment);
|
|
22932
|
-
const currentPreview = previousDeployments.find((release2) => release2.kind === "preview" && matchesSource(release2, commit));
|
|
22933
|
-
if (alreadyPushed && (!currentPreview || currentPreview.status === "failed") && !(matchesSource(status.releases.preview, commit) && status.releases.preview?.status === "succeeded")) await call2(prefix + "/deployments/preview", { method: "POST", body: JSON.stringify({ sourceRevision: "latest" }) });
|
|
22934
|
-
const overview = await waitForPreview(call2, prefix, commit, { timeoutMs: options.timeoutMs, previousDeploymentIds: previousDeployments.filter((deployment) => deployment.status === "failed").map((deployment) => deployment.id), previousBuildRunId: previousBuild.runId ?? null });
|
|
22935
|
-
if (overview.preview.commitSha !== commit) {
|
|
22936
|
-
if (git2(["status", "--porcelain"], root) || git2(["rev-parse", "HEAD"], root) !== commit) throw new Error("Local work changed while the preview built. Review it before synchronizing the build commit.");
|
|
22937
|
-
const refreshed = await call2(prefix + "/developer/git-credential", { method: "POST" });
|
|
22938
|
-
if (refreshed.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed while building.");
|
|
22939
|
-
git2(["fetch", "--", destination, "refs/heads/" + status.branch], root, gitEnvironment(status.repository, refreshed.token));
|
|
22940
|
-
git2(["merge", "--ff-only", overview.preview.commitSha], root);
|
|
23206
|
+
for (const filename2 of pending) {
|
|
23207
|
+
const destination = join16(root, filename2);
|
|
23208
|
+
mkdirSync9(dirname16(destination), { recursive: true });
|
|
23209
|
+
writeFileSync13(destination, files[filename2], { flag: "wx" });
|
|
22941
23210
|
}
|
|
22942
|
-
return
|
|
23211
|
+
return { template: "react-vite-hono", created: pending, unchanged: Object.keys(files).length - pending.length, installed: false, deployed: false };
|
|
22943
23212
|
}
|
|
22944
|
-
|
|
22945
|
-
|
|
22946
|
-
|
|
22947
|
-
|
|
22948
|
-
|
|
22949
|
-
if (
|
|
22950
|
-
const
|
|
22951
|
-
|
|
22952
|
-
|
|
22953
|
-
|
|
22954
|
-
|
|
22955
|
-
|
|
22956
|
-
|
|
22957
|
-
|
|
22958
|
-
|
|
22959
|
-
if (releases.live?.previewDeploymentId !== preview.id || releases.live.status !== "succeeded" || releases.live.healthResult !== "healthy") throw new Error("Production did not activate this healthy release. Run backify status.");
|
|
22960
|
-
return { status: "live", url: releases.live.url };
|
|
23213
|
+
|
|
23214
|
+
// src/local-inspection-command.ts
|
|
23215
|
+
function localInspectionCommand(args2) {
|
|
23216
|
+
const routes = { tables: "/v1/database/tables", rows: "/v1/database/rows", buckets: "/v1/storage/buckets", objects: "/v1/storage/objects", preview: "/v1/storage/preview", events: "/v1/events" };
|
|
23217
|
+
const kind = args2[0];
|
|
23218
|
+
if (!Object.hasOwn(routes, kind)) throw new Error("Use backify inspect tables|rows|buckets|objects|preview|events --local [--json]");
|
|
23219
|
+
const allowed = kind === "rows" ? ["--table", "--limit", "--offset"] : kind === "objects" ? ["--bucket", "--prefix", "--limit", "--offset"] : kind === "preview" ? ["--bucket", "--key"] : [];
|
|
23220
|
+
const values = /* @__PURE__ */ new Map();
|
|
23221
|
+
const flags = /* @__PURE__ */ new Set();
|
|
23222
|
+
for (let index = 1; index < args2.length; index++) {
|
|
23223
|
+
const name2 = args2[index];
|
|
23224
|
+
if (["--local", "--json"].includes(name2)) {
|
|
23225
|
+
if (flags.has(name2)) throw new Error("Duplicate inspection flag");
|
|
23226
|
+
flags.add(name2);
|
|
23227
|
+
continue;
|
|
22961
23228
|
}
|
|
22962
|
-
|
|
22963
|
-
|
|
22964
|
-
|
|
23229
|
+
if (!allowed.includes(name2) || values.has(name2) || args2[index + 1] === void 0 || args2[index + 1].startsWith("--")) throw new Error("Invalid or missing inspection option");
|
|
23230
|
+
values.set(name2, args2[++index]);
|
|
23231
|
+
}
|
|
23232
|
+
if (!flags.has("--local")) throw new Error("Inspection requires --local; no cloud or production data is queried");
|
|
23233
|
+
const required = (name2) => {
|
|
23234
|
+
const value = values.get(name2);
|
|
23235
|
+
if (!value) throw new Error("Required inspection option: " + name2);
|
|
23236
|
+
return value;
|
|
23237
|
+
};
|
|
23238
|
+
const page = () => {
|
|
23239
|
+
const number = (name2, fallback, minimum, maximum) => {
|
|
23240
|
+
const value = values.get(name2);
|
|
23241
|
+
if (value !== void 0 && !/^\d+$/.test(value)) throw new Error("Invalid inspection page");
|
|
23242
|
+
const parsed = value === void 0 ? fallback : Number(value);
|
|
23243
|
+
if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) throw new Error("Invalid inspection page");
|
|
23244
|
+
return parsed;
|
|
23245
|
+
};
|
|
23246
|
+
return { limit: number("--limit", 25, 1, 50), offset: number("--offset", 0, 0, 1e4) };
|
|
23247
|
+
};
|
|
23248
|
+
const input = kind === "rows" ? { table: required("--table"), ...page() } : kind === "objects" ? { bucket: required("--bucket"), ...page(), ...values.has("--prefix") ? { prefix: values.get("--prefix") } : {} } : kind === "preview" ? { bucket: required("--bucket"), key: required("--key") } : void 0;
|
|
23249
|
+
return { kind, path: routes[kind], input };
|
|
22965
23250
|
}
|
|
22966
23251
|
|
|
22967
23252
|
// src/index.ts
|
|
22968
23253
|
import { readFileSync as readFileSync24, existsSync as existsSync15, rmSync as rmSync3 } from "node:fs";
|
|
22969
23254
|
import { homedir as homedir2 } from "node:os";
|
|
22970
23255
|
import { join as join17 } from "node:path";
|
|
22971
|
-
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
22972
|
-
import { createServer as createServer3 } from "node:http";
|
|
22973
23256
|
import { spawn as spawn5 } from "node:child_process";
|
|
22974
23257
|
var configPath = join17(homedir2(), ".backify.json");
|
|
22975
23258
|
var userConfig = existsSync15(configPath) ? JSON.parse(readFileSync24(configPath, "utf8")) : {};
|
|
@@ -22977,73 +23260,42 @@ var [command2, ...args] = process.argv.slice(2);
|
|
|
22977
23260
|
async function call(path, init) {
|
|
22978
23261
|
let appId = option("--app");
|
|
22979
23262
|
if (!appId && existsSync15(join17(process.cwd(), "backify.json"))) appId = projectApplication();
|
|
22980
|
-
const session = appId ? userConfig.sessions?.[
|
|
22981
|
-
|
|
23263
|
+
const session = appId ? userConfig.sessions?.[sessionKey(appId)] : void 0;
|
|
23264
|
+
const credential = session ?? { token: userConfig.token ?? "", organizationId: userConfig.organizationId ?? "", apiUrl: process.env.BACKIFY_API_URL ?? userConfig.apiUrl ?? "https://api.backify.dev/api" };
|
|
23265
|
+
return (appId ? applicationClient(credential, appId) : apiClient(credential))(path, init);
|
|
22982
23266
|
}
|
|
22983
23267
|
var option = (name2) => {
|
|
22984
23268
|
const index = args.indexOf(name2);
|
|
22985
23269
|
return index >= 0 ? args[index + 1] : void 0;
|
|
22986
23270
|
};
|
|
22987
23271
|
function openBrowser(url) {
|
|
22988
|
-
const
|
|
22989
|
-
const child = spawn5(
|
|
23272
|
+
const executable = process.platform === "win32" ? "rundll32.exe" : process.platform === "darwin" ? "open" : "xdg-open", browserArgs = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
23273
|
+
const child = spawn5(executable, browserArgs, { stdio: "ignore", detached: true, windowsHide: true });
|
|
23274
|
+
child.on("error", () => console.error("Could not open a browser automatically. Open the sign-in URL printed above."));
|
|
22990
23275
|
child.unref();
|
|
22991
23276
|
}
|
|
22992
|
-
|
|
22993
|
-
|
|
22994
|
-
|
|
22995
|
-
|
|
22996
|
-
|
|
22997
|
-
|
|
22998
|
-
|
|
22999
|
-
|
|
23000
|
-
|
|
23277
|
+
function sessionKey(appId) {
|
|
23278
|
+
return "app_" + applicationGuid(appId).replaceAll("-", "");
|
|
23279
|
+
}
|
|
23280
|
+
async function browserLogin(appId, organizationId, apiUrl) {
|
|
23281
|
+
if (args.includes("--no-browser")) throw new CliAuthenticationRequired();
|
|
23282
|
+
const address = apiAddress(apiUrl ?? process.env.BACKIFY_API_URL ?? userConfig.apiUrl ?? "https://api.backify.dev/api");
|
|
23283
|
+
return browserAuthenticate({ authUrl: process.env.BACKIFY_AUTH_URL ?? process.env.BACKIFY_PORTAL_URL ?? userConfig.portalUrl ?? "https://auth.backify.dev", apiUrl: address, applicationId: appId ? applicationGuid(appId) : void 0, organizationId, openBrowser: (url) => {
|
|
23284
|
+
console.error("Sign in to reconnect Backify CLI access. Choose the expiration in your browser; the CLI will continue automatically.\n" + url);
|
|
23285
|
+
openBrowser(url);
|
|
23286
|
+
}, save: (session) => {
|
|
23287
|
+
if (appId) {
|
|
23288
|
+
userConfig.sessions ??= {};
|
|
23289
|
+
userConfig.sessions[sessionKey(appId)] = session;
|
|
23290
|
+
} else {
|
|
23291
|
+
userConfig.token = session.token;
|
|
23292
|
+
userConfig.organizationId = session.organizationId;
|
|
23001
23293
|
}
|
|
23002
|
-
|
|
23003
|
-
|
|
23004
|
-
|
|
23005
|
-
|
|
23006
|
-
|
|
23007
|
-
const value = JSON.parse(raw);
|
|
23008
|
-
if (value.state !== state || typeof value.token !== "string" || typeof value.organizationId !== "string") throw new Error("Invalid callback");
|
|
23009
|
-
res.writeHead(200, { "access-control-allow-origin": "*", "content-type": "text/plain" });
|
|
23010
|
-
res.end("Backify CLI connected. You can close this tab.");
|
|
23011
|
-
userConfig.token = value.token;
|
|
23012
|
-
userConfig.organizationId = value.organizationId;
|
|
23013
|
-
saveUserConfig(configPath, userConfig);
|
|
23014
|
-
server.close();
|
|
23015
|
-
resolveLogin();
|
|
23016
|
-
} catch {
|
|
23017
|
-
res.writeHead(400, { "access-control-allow-origin": "*" });
|
|
23018
|
-
res.end("Invalid or expired Backify CLI login.");
|
|
23019
|
-
}
|
|
23020
|
-
});
|
|
23021
|
-
});
|
|
23022
|
-
let resolveLogin = () => {
|
|
23023
|
-
};
|
|
23024
|
-
const completed = new Promise((resolve24) => resolveLogin = resolve24);
|
|
23025
|
-
server.listen(0, "127.0.0.1", () => {
|
|
23026
|
-
const address = server.address();
|
|
23027
|
-
if (!address || typeof address === "string") throw new Error("Could not start local login callback");
|
|
23028
|
-
const callback = `http://127.0.0.1:${address.port}/callback`;
|
|
23029
|
-
const portal = new URL(process.env.BACKIFY_PORTAL_URL ?? userConfig.portalUrl ?? "https://backify.dev");
|
|
23030
|
-
portal.searchParams.set("cli_callback", callback);
|
|
23031
|
-
portal.searchParams.set("cli_state", state);
|
|
23032
|
-
console.log(`Opening ${portal.origin} to sign in to Backify\u2026`);
|
|
23033
|
-
openBrowser(portal.toString());
|
|
23034
|
-
});
|
|
23035
|
-
const timeout = setTimeout(() => {
|
|
23036
|
-
server.close();
|
|
23037
|
-
rejectLogin(new Error("Backify login timed out. Run backify login to try again."));
|
|
23038
|
-
}, 5 * 6e4);
|
|
23039
|
-
let rejectLogin = () => {
|
|
23040
|
-
};
|
|
23041
|
-
const failed = new Promise((_, reject) => rejectLogin = reject);
|
|
23042
|
-
try {
|
|
23043
|
-
await Promise.race([completed, failed]);
|
|
23044
|
-
} finally {
|
|
23045
|
-
clearTimeout(timeout);
|
|
23046
|
-
}
|
|
23294
|
+
saveUserConfig(configPath, userConfig);
|
|
23295
|
+
} });
|
|
23296
|
+
}
|
|
23297
|
+
function applicationClient(session, appId) {
|
|
23298
|
+
return reconnectingApiClient(session, () => browserLogin(appId, session.organizationId, session.apiUrl));
|
|
23047
23299
|
}
|
|
23048
23300
|
async function main() {
|
|
23049
23301
|
switch (command2) {
|
|
@@ -23191,11 +23443,11 @@ async function main() {
|
|
|
23191
23443
|
break;
|
|
23192
23444
|
}
|
|
23193
23445
|
case "login":
|
|
23194
|
-
if (args[0]) {
|
|
23446
|
+
if (args[0] && !args[0].startsWith("--")) {
|
|
23195
23447
|
userConfig.token = args[0];
|
|
23196
23448
|
userConfig.organizationId = args[1] ?? userConfig.organizationId;
|
|
23197
23449
|
saveUserConfig(configPath, userConfig);
|
|
23198
|
-
} else await browserLogin();
|
|
23450
|
+
} else await browserLogin(option("--app") ?? (existsSync15(join17(process.cwd(), "backify.json")) ? projectApplication() : void 0), option("--organization"));
|
|
23199
23451
|
console.log("Logged in to Backify");
|
|
23200
23452
|
break;
|
|
23201
23453
|
case "logout":
|
|
@@ -23204,7 +23456,7 @@ async function main() {
|
|
|
23204
23456
|
break;
|
|
23205
23457
|
case "connect": {
|
|
23206
23458
|
const appId = option("--app") ?? projectApplication(), key = "app_" + applicationGuid(appId).replaceAll("-", "");
|
|
23207
|
-
const code = option("--pair"), organizationId = option("--organization") ?? userConfig.organizationId;
|
|
23459
|
+
const code = option("--pair"), organizationId = option("--organization") ?? userConfig.sessions?.[key]?.organizationId ?? userConfig.organizationId;
|
|
23208
23460
|
const address = apiAddress(option("--api") ?? process.env.BACKIFY_API_URL ?? userConfig.apiUrl ?? "https://api.backify.dev/api");
|
|
23209
23461
|
if (code) {
|
|
23210
23462
|
if (!organizationId) throw new Error("The setup prompt must supply the organization.");
|
|
@@ -23213,20 +23465,25 @@ async function main() {
|
|
|
23213
23465
|
userConfig.sessions ??= {};
|
|
23214
23466
|
userConfig.sessions[key] = { ...paired, apiUrl: address };
|
|
23215
23467
|
saveUserConfig(configPath, userConfig);
|
|
23216
|
-
} else if (!userConfig.sessions?.[key] && !userConfig.token) {
|
|
23217
|
-
await browserLogin();
|
|
23468
|
+
} else if (args.includes("--reauth") || !userConfig.sessions?.[key] && !userConfig.token) {
|
|
23469
|
+
await browserLogin(appId, organizationId, address);
|
|
23218
23470
|
}
|
|
23219
23471
|
let session = userConfig.sessions?.[key] ?? { token: userConfig.token ?? "", organizationId: organizationId ?? userConfig.organizationId ?? "", apiUrl: address };
|
|
23220
23472
|
if (!session.token.startsWith("bfcli_")) {
|
|
23221
|
-
|
|
23473
|
+
try {
|
|
23474
|
+
session = await registerCliSession(apiClient(session), applicationGuid(appId), session.apiUrl);
|
|
23475
|
+
} catch (error) {
|
|
23476
|
+
if (!(error instanceof CliAuthenticationRequired)) throw error;
|
|
23477
|
+
session = await browserLogin(appId, session.organizationId, session.apiUrl);
|
|
23478
|
+
}
|
|
23222
23479
|
userConfig.sessions ??= {};
|
|
23223
23480
|
userConfig.sessions[key] = session;
|
|
23224
23481
|
saveUserConfig(configPath, userConfig);
|
|
23225
23482
|
}
|
|
23226
|
-
const client =
|
|
23483
|
+
const client = applicationClient(session, appId);
|
|
23227
23484
|
await client("/apps/" + applicationGuid(appId) + "/developer");
|
|
23228
23485
|
userConfig.sessions ??= {};
|
|
23229
|
-
userConfig.sessions[key]
|
|
23486
|
+
userConfig.sessions[key] ??= session;
|
|
23230
23487
|
saveUserConfig(configPath, userConfig);
|
|
23231
23488
|
if (args.includes("--clone")) await cloneProject(client, appId);
|
|
23232
23489
|
else if (existsSync15(join17(process.cwd(), ".git"))) {
|
|
@@ -23260,7 +23517,7 @@ async function main() {
|
|
|
23260
23517
|
const latest = deployments[0];
|
|
23261
23518
|
if (latest?.status === "failed") console.log(JSON.stringify(await call(prefix + "/deployments/" + latest.id), null, 2));
|
|
23262
23519
|
try {
|
|
23263
|
-
const commit =
|
|
23520
|
+
const commit = git(["rev-parse", "HEAD"]);
|
|
23264
23521
|
console.log(JSON.stringify(await call(prefix + "/developer/build?commitSha=" + commit), null, 2));
|
|
23265
23522
|
} catch {
|
|
23266
23523
|
console.log("Build status is unavailable for the current local commit.");
|
|
@@ -23340,7 +23597,7 @@ async function main() {
|
|
|
23340
23597
|
}
|
|
23341
23598
|
main().catch((error) => {
|
|
23342
23599
|
const message = error instanceof Error ? error.message : String(error);
|
|
23343
|
-
if (args.includes("--json")) console.log(JSON.stringify({ schemaVersion: 1, status: "failed", exitCode: 1, error: message }));
|
|
23600
|
+
if (args.includes("--json")) console.log(JSON.stringify({ schemaVersion: 1, status: "failed", exitCode: 1, error: message, ...error instanceof CliAuthenticationRequired ? { code: error.code, reauthenticationCommand: "backify connect --reauth" + (option("--app") ? " --app " + applicationGuid(option("--app")) : "") } : {} }));
|
|
23344
23601
|
else console.error(message);
|
|
23345
23602
|
process.exitCode = 1;
|
|
23346
23603
|
});
|