@backifydev/cli 0.3.7 → 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 +5 -1
- package/dist/index.js +1537 -1209
- 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
|
}
|
|
@@ -17660,154 +17660,85 @@ async function packageFunctions(entry, output) {
|
|
|
17660
17660
|
return outputPath;
|
|
17661
17661
|
}
|
|
17662
17662
|
|
|
17663
|
-
// src/
|
|
17664
|
-
import {
|
|
17665
|
-
import { chmodSync, existsSync as existsSync2, lstatSync, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
17666
|
-
import { dirname as dirname2, join, resolve as resolve2, sep } from "node:path";
|
|
17667
|
-
import { homedir } from "node:os";
|
|
17668
|
-
import { spawnSync } from "node:child_process";
|
|
17663
|
+
// src/cli-session.ts
|
|
17664
|
+
import { hostname, platform, release, arch } from "node:os";
|
|
17669
17665
|
|
|
17670
|
-
//
|
|
17671
|
-
|
|
17672
|
-
|
|
17673
|
-
|
|
17674
|
-
|
|
17675
|
-
|
|
17676
|
-
|
|
17677
|
-
|
|
17678
|
-
|
|
17679
|
-
|
|
17680
|
-
|
|
17666
|
+
// package.json
|
|
17667
|
+
var package_default = {
|
|
17668
|
+
name: "@backifydev/cli",
|
|
17669
|
+
version: "0.3.9",
|
|
17670
|
+
description: "Command-line setup and diagnostics for Backify applications",
|
|
17671
|
+
type: "module",
|
|
17672
|
+
bin: {
|
|
17673
|
+
backify: "dist/index.js"
|
|
17674
|
+
},
|
|
17675
|
+
files: [
|
|
17676
|
+
"dist/index.js",
|
|
17677
|
+
"README.md"
|
|
17678
|
+
],
|
|
17679
|
+
scripts: {
|
|
17680
|
+
prebuild: "npm run build -w @backify/platform && npm run build -w @backifydev/sdk && npm run build -w @backify/analyzer",
|
|
17681
|
+
build: `esbuild src/index.ts --bundle --platform=node --format=esm --external:esbuild --external:typescript --outfile=dist/index.js --banner:js="import { createRequire as __backifyCreateRequire } from 'node:module'; var require = __backifyCreateRequire(import.meta.url);"`,
|
|
17682
|
+
prepack: "npm run build",
|
|
17683
|
+
pretypecheck: "npm run build -w @backify/platform && npm run build -w @backifydev/sdk && npm run build -w @backify/analyzer",
|
|
17684
|
+
typecheck: "tsc --noEmit",
|
|
17685
|
+
pretest: "npm run build -w @backify/platform && npm run build -w @backifydev/sdk && npm run build -w @backify/analyzer",
|
|
17686
|
+
test: "vitest run"
|
|
17687
|
+
},
|
|
17688
|
+
publishConfig: {
|
|
17689
|
+
access: "public"
|
|
17690
|
+
},
|
|
17691
|
+
repository: {
|
|
17692
|
+
type: "git",
|
|
17693
|
+
url: "git+https://github.com/frejnorling/backify.git",
|
|
17694
|
+
directory: "packages/cli"
|
|
17695
|
+
},
|
|
17696
|
+
homepage: "https://agent.backify.dev/",
|
|
17697
|
+
license: "MIT",
|
|
17698
|
+
engines: {
|
|
17699
|
+
node: ">=20"
|
|
17700
|
+
},
|
|
17701
|
+
dependencies: {
|
|
17702
|
+
"@azure/functions": "^4.7.2",
|
|
17703
|
+
"@backifydev/sdk": "^0.1.1",
|
|
17704
|
+
esbuild: "^0.25.10",
|
|
17705
|
+
fflate: "^0.8.2",
|
|
17706
|
+
typescript: "^5.9.2"
|
|
17707
|
+
},
|
|
17708
|
+
devDependencies: {
|
|
17709
|
+
"@backify/analyzer": "*",
|
|
17710
|
+
"@backify/platform": "*",
|
|
17711
|
+
"@hono/node-server": "2.1.3",
|
|
17712
|
+
"@types/node": "^24.3.1",
|
|
17713
|
+
hono: "4.13.12",
|
|
17714
|
+
vitest: "^4.1.11"
|
|
17681
17715
|
}
|
|
17682
|
-
}
|
|
17683
|
-
function renameLocalFile(source, destination) {
|
|
17684
|
-
replaceLocalFile(source, destination, renameSync, process.platform, (milliseconds) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds));
|
|
17685
|
-
}
|
|
17716
|
+
};
|
|
17686
17717
|
|
|
17687
|
-
// src/
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
var hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
17691
|
-
function runtimeIdentifier(platform = process.platform, architecture = process.arch) {
|
|
17692
|
-
const system = { win32: "win", darwin: "osx", linux: "linux" }[platform];
|
|
17693
|
-
if (!system || !["x64", "arm64"].includes(architecture)) throw new Error("Local runtime supports Windows, macOS and Linux on x64/arm64");
|
|
17694
|
-
return system + "-" + architecture;
|
|
17695
|
-
}
|
|
17696
|
-
function checkedPath(root, filename2) {
|
|
17697
|
-
const target = resolve2(root, filename2);
|
|
17698
|
-
if (target !== resolve2(root) && !target.startsWith(resolve2(root) + sep)) throw new Error("Runtime path escapes its installation");
|
|
17699
|
-
let current = target;
|
|
17700
|
-
while (true) {
|
|
17701
|
-
if (existsSync2(current) && lstatSync(current).isSymbolicLink()) throw new Error("Runtime installation must not follow links");
|
|
17702
|
-
if (dirname2(current) === current) break;
|
|
17703
|
-
current = dirname2(current);
|
|
17704
|
-
}
|
|
17705
|
-
return target;
|
|
17706
|
-
}
|
|
17707
|
-
function verifyInstallationFiles(root, expected, directory2 = root) {
|
|
17708
|
-
for (const entry of readdirSync(directory2, { withFileTypes: true })) {
|
|
17709
|
-
const path = checkedPath(root, join(directory2, entry.name));
|
|
17710
|
-
if (entry.isDirectory()) verifyInstallationFiles(root, expected, path);
|
|
17711
|
-
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");
|
|
17712
|
-
}
|
|
17713
|
-
}
|
|
17714
|
-
function installedLocalRuntime(base = join(homedir(), ".backify")) {
|
|
17715
|
-
const pointer = checkedPath(base, "runtime.json");
|
|
17716
|
-
if (!existsSync2(pointer)) return void 0;
|
|
17717
|
-
const installed = JSON.parse(readFileSync(pointer, "utf8"));
|
|
17718
|
-
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");
|
|
17719
|
-
const root = checkedPath(base, join("runtimes", installed.version, installed.rid));
|
|
17720
|
-
verifyInstallationFiles(root, installed.files);
|
|
17721
|
-
for (const [filename2, digest4] of Object.entries(installed.files)) {
|
|
17722
|
-
const path = checkedPath(root, filename2);
|
|
17723
|
-
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");
|
|
17724
|
-
}
|
|
17725
|
-
const executableName = installed.rid.startsWith("win-") ? "Backify.Hcr.exe" : "Backify.Hcr";
|
|
17726
|
-
if (!installed.files["Backify.Hcr.dll"] || !installed.files[executableName]) throw new Error("Runtime archive requires the self-contained HCR executable and DLL");
|
|
17727
|
-
return { path: join(root, "Backify.Hcr.dll"), executable: join(root, executableName), version: installed.version, rid: installed.rid };
|
|
17728
|
-
}
|
|
17729
|
-
function installLocalRuntime(archive, options) {
|
|
17730
|
-
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");
|
|
17731
|
-
if (archive.byteLength > maxArchiveBytes || hash(archive) !== options.sha256) throw new Error("Runtime archive digest does not match the reviewed SHA-256");
|
|
17732
|
-
let expanded = 0, count = 0;
|
|
17733
|
-
const files = unzipSync(archive, { filter: (file) => {
|
|
17734
|
-
if (++count > 3e3 || (expanded += file.originalSize) > maxExpandedBytes || file.originalSize > maxArchiveBytes) throw new Error("Runtime archive exceeds extraction limits");
|
|
17735
|
-
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");
|
|
17736
|
-
return !file.name.endsWith("/");
|
|
17737
|
-
} });
|
|
17738
|
-
const rid = runtimeIdentifier();
|
|
17739
|
-
const executable = rid.startsWith("win-") ? "Backify.Hcr.exe" : "Backify.Hcr";
|
|
17740
|
-
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");
|
|
17741
|
-
const base = options.base ?? join(homedir(), ".backify");
|
|
17742
|
-
const parent = checkedPath(base, join("runtimes", options.version));
|
|
17743
|
-
mkdirSync2(parent, { recursive: true, mode: 448 });
|
|
17744
|
-
const destination = checkedPath(parent, rid);
|
|
17745
|
-
const receipt = { version: options.version, rid, files: Object.fromEntries(Object.entries(files).map(([name2, bytes]) => [name2, hash(bytes)])) };
|
|
17746
|
-
if (!existsSync2(destination)) {
|
|
17747
|
-
const staging = checkedPath(parent, ".install-" + randomUUID());
|
|
17748
|
-
mkdirSync2(staging, { mode: 448 });
|
|
17749
|
-
for (const [name2, bytes] of Object.entries(files)) {
|
|
17750
|
-
const filename2 = checkedPath(staging, name2);
|
|
17751
|
-
mkdirSync2(dirname2(filename2), { recursive: true, mode: 448 });
|
|
17752
|
-
writeFileSync2(filename2, bytes, { flag: "wx", mode: name2 === executable ? 448 : 384 });
|
|
17753
|
-
}
|
|
17754
|
-
if (process.platform !== "win32") chmodSync(join(staging, executable), 448);
|
|
17755
|
-
renameLocalFile(staging, destination);
|
|
17756
|
-
} else {
|
|
17757
|
-
verifyInstallationFiles(destination, receipt.files);
|
|
17758
|
-
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");
|
|
17759
|
-
}
|
|
17760
|
-
const pointer = checkedPath(base, "runtime.json");
|
|
17761
|
-
const temporary = checkedPath(base, ".runtime-" + randomUUID() + ".json");
|
|
17762
|
-
writeFileSync2(temporary, JSON.stringify(receipt), { flag: "wx", mode: 384 });
|
|
17763
|
-
renameLocalFile(temporary, pointer);
|
|
17764
|
-
return installedLocalRuntime(base);
|
|
17765
|
-
}
|
|
17766
|
-
async function downloadLocalRuntime(version) {
|
|
17767
|
-
if (!/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(version)) throw new Error("Provide a release version vX.Y.Z");
|
|
17768
|
-
let url = new URL("https://github.com/frejnorling/backify/releases/download/runtime-" + version + "/backify-hcr-" + runtimeIdentifier() + ".zip");
|
|
17769
|
-
for (let redirects = 0; redirects < 5; redirects++) {
|
|
17770
|
-
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");
|
|
17771
|
-
const response = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(6e4) });
|
|
17772
|
-
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
17773
|
-
url = new URL(response.headers.get("location") ?? "", url);
|
|
17774
|
-
continue;
|
|
17775
|
-
}
|
|
17776
|
-
if (!response.ok || !response.body) throw new Error("The requested runtime release is unavailable");
|
|
17777
|
-
const chunks = [];
|
|
17778
|
-
let size = 0;
|
|
17779
|
-
for await (const chunk of response.body) {
|
|
17780
|
-
size += chunk.byteLength;
|
|
17781
|
-
if (size > maxArchiveBytes) throw new Error("Runtime archive exceeds its size limit");
|
|
17782
|
-
chunks.push(chunk);
|
|
17783
|
-
}
|
|
17784
|
-
return Buffer.concat(chunks);
|
|
17785
|
-
}
|
|
17786
|
-
throw new Error("Too many runtime download redirects");
|
|
17718
|
+
// src/cli-session.ts
|
|
17719
|
+
function cliDevice() {
|
|
17720
|
+
return { deviceName: hostname(), operatingSystem: platform() + " " + release(), architecture: arch(), cliVersion: package_default.version };
|
|
17787
17721
|
}
|
|
17788
|
-
|
|
17789
|
-
|
|
17790
|
-
const
|
|
17791
|
-
|
|
17792
|
-
const executable = directory2 ? join(directory2, name2 + (process.platform === "win32" ? ".exe" : "")) : name2;
|
|
17793
|
-
const result = spawnSync(executable, ["--version"], { windowsHide: true, timeout: 1e4, encoding: "utf8" });
|
|
17794
|
-
return { name: name2, available: result.status === 0 };
|
|
17795
|
-
});
|
|
17796
|
-
return { rid: runtimeIdentifier(), runtime: runtime ? { installed: true, version: runtime.version } : { installed: false }, postgres: checks, ready: !!runtime && checks.every((check) => check.available) };
|
|
17722
|
+
var cliUserAgent = "BackifyCLI/" + package_default.version;
|
|
17723
|
+
async function registerCliSession(client, applicationId, apiUrl) {
|
|
17724
|
+
const session = await client("/apps/" + applicationId + "/developer/session", { method: "POST", body: JSON.stringify(cliDevice()) });
|
|
17725
|
+
return { ...session, apiUrl };
|
|
17797
17726
|
}
|
|
17798
17727
|
|
|
17799
|
-
// src/
|
|
17800
|
-
import {
|
|
17801
|
-
import {
|
|
17802
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync16, realpathSync as realpathSync6, readdirSync as readdirSync4, writeFileSync as writeFileSync8 } from "node:fs";
|
|
17803
|
-
import { dirname as dirname10, isAbsolute as isAbsolute13, join as join10, relative as relative8, resolve as resolve16 } from "node:path";
|
|
17728
|
+
// src/browser-auth.ts
|
|
17729
|
+
import { randomBytes } from "node:crypto";
|
|
17730
|
+
import { createServer } from "node:http";
|
|
17804
17731
|
|
|
17805
|
-
// src/
|
|
17806
|
-
import {
|
|
17807
|
-
import {
|
|
17808
|
-
import {
|
|
17809
|
-
import {
|
|
17810
|
-
|
|
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";
|
|
17737
|
+
|
|
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";
|
|
17811
17742
|
|
|
17812
17743
|
// ../platform/dist/manifest.js
|
|
17813
17744
|
var import_yaml = __toESM(require_dist(), 1);
|
|
@@ -17840,6 +17771,8 @@ function parseApplicationManifest(source) {
|
|
|
17840
17771
|
if (!object(settings))
|
|
17841
17772
|
throw new Error("Capability configuration must be a mapping: " + name2);
|
|
17842
17773
|
validatePortableSettings(settings);
|
|
17774
|
+
if (name2 === "auth")
|
|
17775
|
+
validateAuthSettings(settings);
|
|
17843
17776
|
if (settings.enabled !== void 0 && typeof settings.enabled !== "boolean")
|
|
17844
17777
|
throw new Error("enabled must be boolean: " + name2);
|
|
17845
17778
|
if (name2 === "database" && settings.engine !== void 0 && settings.engine !== "postgres")
|
|
@@ -17847,7 +17780,7 @@ function parseApplicationManifest(source) {
|
|
|
17847
17780
|
if (name2 === "database" && settings.migrations !== void 0 && !portablePath(settings.migrations))
|
|
17848
17781
|
throw new Error("Migrations must be repository-relative");
|
|
17849
17782
|
if (name2 === "storage" && settings.buckets !== void 0) {
|
|
17850
|
-
if (!Array.isArray(settings.buckets) || settings.buckets.length > 50 || settings.buckets.some((bucket) => !object(bucket) || typeof bucket.name !== "string" || !/^[A-Za-z0-9_-]{1,100}$/.test(bucket.name) || bucket.access !== void 0 && bucket.access !== "private" || Object.keys(bucket).some((key) => !["name", "access"].includes(key))) || new Set(settings.buckets.map((bucket) => bucket.name)).size !== settings.buckets.length)
|
|
17783
|
+
if (!Array.isArray(settings.buckets) || settings.buckets.length > 50 || settings.buckets.some((bucket) => !object(bucket) || typeof bucket.name !== "string" || !/^[A-Za-z0-9_-]{1,100}$/.test(bucket.name) || bucket.access !== void 0 && bucket.access !== "private" || bucket.isolation !== void 0 && !["private", "user", "tenant", "authenticated", "public", "selected"].includes(String(bucket.isolation)) || bucket.allowSignedUrls !== void 0 && typeof bucket.allowSignedUrls !== "boolean" || Object.keys(bucket).some((key) => !["name", "access", "isolation", "allowSignedUrls"].includes(key))) || new Set(settings.buckets.map((bucket) => bucket.name)).size !== settings.buckets.length)
|
|
17851
17784
|
throw new Error("Storage requires at most 50 distinct private logical buckets");
|
|
17852
17785
|
}
|
|
17853
17786
|
capabilities[name2] = structuredClone(settings);
|
|
@@ -17869,8 +17802,32 @@ function validatePortableSettings(settings) {
|
|
|
17869
17802
|
}
|
|
17870
17803
|
}
|
|
17871
17804
|
}
|
|
17872
|
-
function
|
|
17873
|
-
|
|
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;
|
|
17874
17831
|
}
|
|
17875
17832
|
|
|
17876
17833
|
// ../platform/dist/delivery.js
|
|
@@ -17903,6 +17860,10 @@ Run the following to discover the connected hosting target, repository, branch,
|
|
|
17903
17860
|
|
|
17904
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.
|
|
17905
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
|
+
|
|
17906
17867
|
## Record each user request
|
|
17907
17868
|
|
|
17908
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:
|
|
@@ -17984,7 +17945,7 @@ var MAIL_LIMITS = Object.freeze({
|
|
|
17984
17945
|
});
|
|
17985
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 });
|
|
17986
17947
|
var CAPABILITIES = [
|
|
17987
|
-
capability("auth", "auth", "Application users, organizations, roles, and permissions"),
|
|
17948
|
+
capability("auth", "auth", "Application users, organizations, roles, and permissions", true),
|
|
17988
17949
|
capability("database", "db", "Persistent application data", true),
|
|
17989
17950
|
capability("storage", "storage", "Persistent files and objects", true),
|
|
17990
17951
|
capability("functions", "functions", "Server function declarations"),
|
|
@@ -18098,114 +18059,932 @@ function validateConfig(value) {
|
|
|
18098
18059
|
}
|
|
18099
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 } } } } };
|
|
18100
18061
|
|
|
18101
|
-
//
|
|
18102
|
-
import
|
|
18103
|
-
import {
|
|
18104
|
-
import {
|
|
18105
|
-
var
|
|
18106
|
-
function
|
|
18107
|
-
const
|
|
18108
|
-
|
|
18109
|
-
|
|
18110
|
-
|
|
18111
|
-
|
|
18112
|
-
|
|
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
|
+
}
|
|
18113
18075
|
}
|
|
18114
|
-
function
|
|
18115
|
-
|
|
18116
|
-
|
|
18117
|
-
|
|
18118
|
-
|
|
18119
|
-
const
|
|
18120
|
-
|
|
18121
|
-
|
|
18122
|
-
|
|
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;
|
|
18123
18109
|
}
|
|
18124
|
-
|
|
18125
|
-
|
|
18126
|
-
|
|
18127
|
-
|
|
18128
|
-
|
|
18129
|
-
|
|
18130
|
-
|
|
18131
|
-
|
|
18132
|
-
|
|
18133
|
-
|
|
18134
|
-
|
|
18135
|
-
|
|
18136
|
-
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
}
|
|
18143
|
-
}
|
|
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") : "") };
|
|
18144
18128
|
});
|
|
18145
|
-
const
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
used.add("scheduler");
|
|
18150
|
-
if (ts.isIdentifier(node)) {
|
|
18151
|
-
const parent = node.parent;
|
|
18152
|
-
if (named.has(node.text) && !ts.isImportSpecifier(parent) && isValueUse(node))
|
|
18153
|
-
used.add(named.get(node.text));
|
|
18154
|
-
}
|
|
18155
|
-
if (ts.isPropertyAccessExpression(node)) {
|
|
18156
|
-
if (ts.isIdentifier(node.expression)) {
|
|
18157
|
-
if (namespaces.has(node.expression.text)) {
|
|
18158
|
-
const cap = SDK_IMPORT_CAPABILITIES[node.name.text];
|
|
18159
|
-
if (cap)
|
|
18160
|
-
used.add(cap);
|
|
18161
|
-
}
|
|
18162
|
-
if (roots.has(node.expression.text)) {
|
|
18163
|
-
const cap = SDK_IMPORT_CAPABILITIES[node.name.text];
|
|
18164
|
-
if (cap)
|
|
18165
|
-
used.add(cap);
|
|
18166
|
-
}
|
|
18167
|
-
}
|
|
18168
|
-
if (node.name.text === "schedule" && rootCapability(node.expression, named, namespaces, roots) === "jobs")
|
|
18169
|
-
used.add("scheduler");
|
|
18170
|
-
}
|
|
18171
|
-
ts.forEachChild(node, visit);
|
|
18172
|
-
};
|
|
18173
|
-
visit(source);
|
|
18129
|
+
for (const { path, content } of updates) {
|
|
18130
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
18131
|
+
writeFileSync3(path, content);
|
|
18132
|
+
}
|
|
18174
18133
|
}
|
|
18175
|
-
function
|
|
18176
|
-
const
|
|
18177
|
-
|
|
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");
|
|
18178
18148
|
}
|
|
18179
|
-
function
|
|
18180
|
-
|
|
18181
|
-
|
|
18182
|
-
|
|
18183
|
-
|
|
18184
|
-
|
|
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;
|
|
18185
18155
|
}
|
|
18186
|
-
return void 0;
|
|
18187
18156
|
}
|
|
18188
|
-
function
|
|
18189
|
-
|
|
18190
|
-
const
|
|
18191
|
-
|
|
18192
|
-
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".backify" || entry.name.startsWith(".git"))
|
|
18193
|
-
continue;
|
|
18194
|
-
const path = join2(dir, entry.name);
|
|
18195
|
-
if (entry.isDirectory())
|
|
18196
|
-
walk(path);
|
|
18197
|
-
else if ([".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs"].includes(extname(path)))
|
|
18198
|
-
result.push({ path, content: readFileSync2(path, "utf8") });
|
|
18199
|
-
}
|
|
18200
|
-
};
|
|
18201
|
-
if (existsSync3(root))
|
|
18202
|
-
walk(root);
|
|
18203
|
-
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}`);
|
|
18204
18161
|
}
|
|
18205
|
-
|
|
18206
|
-
|
|
18207
|
-
|
|
18208
|
-
const
|
|
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) {
|
|
18987
|
+
const tableName = (value) => typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]{0,62}$/.test(value) && !/^__backify/i.test(value);
|
|
18209
18988
|
const bucketName = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,100}$/.test(value);
|
|
18210
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");
|
|
18211
18990
|
const fail = () => {
|
|
@@ -18407,9 +19186,9 @@ function createLocalNativeEvents(local, native) {
|
|
|
18407
19186
|
|
|
18408
19187
|
// src/local-job-executor.ts
|
|
18409
19188
|
import { spawn as spawn2 } from "node:child_process";
|
|
18410
|
-
import { createHash as
|
|
18411
|
-
import { lstatSync as
|
|
18412
|
-
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";
|
|
18413
19192
|
|
|
18414
19193
|
// ../sdk/dist/schedules.js
|
|
18415
19194
|
var import_cron_parser = __toESM(require_dist2(), 1);
|
|
@@ -18620,28 +19399,28 @@ function createApplicationAuthProvider(source, options) {
|
|
|
18620
19399
|
}
|
|
18621
19400
|
|
|
18622
19401
|
// ../sdk/dist/application-auth-state.js
|
|
18623
|
-
import { createHash as
|
|
18624
|
-
import { closeSync, constants, fstatSync, fsyncSync, lstatSync as
|
|
18625
|
-
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";
|
|
18626
19405
|
function unavailable2() {
|
|
18627
19406
|
throw new ApplicationAuthenticationError("application_identity_unavailable");
|
|
18628
19407
|
}
|
|
18629
19408
|
function safePath(filename2) {
|
|
18630
|
-
if (!
|
|
19409
|
+
if (!isAbsolute2(filename2) || resolve5(filename2) !== filename2 || filename2 === parse(filename2).root)
|
|
18631
19410
|
unavailable2();
|
|
18632
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)))
|
|
18633
19412
|
unavailable2();
|
|
18634
|
-
const directory2 =
|
|
19413
|
+
const directory2 = dirname6(filename2);
|
|
18635
19414
|
let ancestor = directory2;
|
|
18636
19415
|
for (; ; ) {
|
|
18637
|
-
const metadata =
|
|
19416
|
+
const metadata = lstatSync4(ancestor);
|
|
18638
19417
|
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
18639
19418
|
unavailable2();
|
|
18640
19419
|
if (ancestor === parse(ancestor).root)
|
|
18641
19420
|
break;
|
|
18642
|
-
ancestor =
|
|
19421
|
+
ancestor = dirname6(ancestor);
|
|
18643
19422
|
}
|
|
18644
|
-
const parent =
|
|
19423
|
+
const parent = lstatSync4(directory2);
|
|
18645
19424
|
if (process.platform !== "win32" && ((parent.mode & 63) !== 0 || parent.uid !== process.getuid?.()))
|
|
18646
19425
|
unavailable2();
|
|
18647
19426
|
}
|
|
@@ -18652,7 +19431,7 @@ function safeFile(metadata) {
|
|
|
18652
19431
|
function syncDirectory(filename2) {
|
|
18653
19432
|
if (process.platform === "win32")
|
|
18654
19433
|
return;
|
|
18655
|
-
const descriptor2 = openSync(
|
|
19434
|
+
const descriptor2 = openSync(dirname6(filename2), constants.O_RDONLY | constants.O_DIRECTORY);
|
|
18656
19435
|
try {
|
|
18657
19436
|
fsyncSync(descriptor2);
|
|
18658
19437
|
} finally {
|
|
@@ -18660,7 +19439,7 @@ function syncDirectory(filename2) {
|
|
|
18660
19439
|
}
|
|
18661
19440
|
}
|
|
18662
19441
|
function readState(filename2) {
|
|
18663
|
-
const metadata =
|
|
19442
|
+
const metadata = lstatSync4(filename2);
|
|
18664
19443
|
safeFile(metadata);
|
|
18665
19444
|
if (metadata.size > 2048)
|
|
18666
19445
|
unavailable2();
|
|
@@ -18693,30 +19472,30 @@ function readState(filename2) {
|
|
|
18693
19472
|
}
|
|
18694
19473
|
}
|
|
18695
19474
|
function persist(filename2, state) {
|
|
18696
|
-
const temporary = filename2 + "." +
|
|
19475
|
+
const temporary = filename2 + "." + randomUUID3() + ".tmp";
|
|
18697
19476
|
const descriptor2 = openSync(temporary, "wx", 384);
|
|
18698
19477
|
try {
|
|
18699
|
-
|
|
19478
|
+
writeFileSync6(descriptor2, JSON.stringify(state));
|
|
18700
19479
|
fsyncSync(descriptor2);
|
|
18701
19480
|
} finally {
|
|
18702
19481
|
closeSync(descriptor2);
|
|
18703
19482
|
}
|
|
18704
|
-
|
|
19483
|
+
renameSync3(temporary, filename2);
|
|
18705
19484
|
syncDirectory(filename2);
|
|
18706
19485
|
}
|
|
18707
19486
|
function checkpoint(filename2, trust, initialize2) {
|
|
18708
19487
|
try {
|
|
18709
|
-
if (!
|
|
19488
|
+
if (!isAbsolute2(filename2))
|
|
18710
19489
|
unavailable2();
|
|
18711
|
-
filename2 =
|
|
19490
|
+
filename2 = resolve5(filename2);
|
|
18712
19491
|
safePath(filename2);
|
|
18713
19492
|
const lock = filename2 + ".lock";
|
|
18714
|
-
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") };
|
|
18715
19494
|
if (!initialize2) {
|
|
18716
19495
|
const previous = readState(filename2);
|
|
18717
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) {
|
|
18718
19497
|
try {
|
|
18719
|
-
|
|
19498
|
+
lstatSync4(lock);
|
|
18720
19499
|
} catch (error) {
|
|
18721
19500
|
if (error.code === "ENOENT")
|
|
18722
19501
|
return;
|
|
@@ -18727,7 +19506,7 @@ function checkpoint(filename2, trust, initialize2) {
|
|
|
18727
19506
|
}
|
|
18728
19507
|
const descriptor2 = openSync(lock, "wx", 384);
|
|
18729
19508
|
try {
|
|
18730
|
-
|
|
19509
|
+
writeFileSync6(descriptor2, "backify-application-auth-state-v1");
|
|
18731
19510
|
fsyncSync(descriptor2);
|
|
18732
19511
|
} finally {
|
|
18733
19512
|
closeSync(descriptor2);
|
|
@@ -18737,7 +19516,7 @@ function checkpoint(filename2, trust, initialize2) {
|
|
|
18737
19516
|
try {
|
|
18738
19517
|
if (initialize2) {
|
|
18739
19518
|
try {
|
|
18740
|
-
|
|
19519
|
+
lstatSync4(filename2);
|
|
18741
19520
|
unavailable2();
|
|
18742
19521
|
} catch (error) {
|
|
18743
19522
|
if (error.code !== "ENOENT")
|
|
@@ -18774,18 +19553,18 @@ function createPersistentApplicationAuthProvider(source, options) {
|
|
|
18774
19553
|
}
|
|
18775
19554
|
|
|
18776
19555
|
// ../sdk/dist/application-auth-sync.js
|
|
18777
|
-
import { createHash as
|
|
18778
|
-
import { closeSync as closeSync2, constants as constants2, fsyncSync as fsyncSync2, lstatSync as
|
|
18779
|
-
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";
|
|
18780
19559
|
var maximumBytes = 512 * 1024;
|
|
18781
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";
|
|
18782
19561
|
function unavailable3() {
|
|
18783
19562
|
throw new ApplicationAuthenticationError("application_identity_unavailable");
|
|
18784
19563
|
}
|
|
18785
19564
|
function filename(value) {
|
|
18786
|
-
if (typeof value !== "string" || /[\x00-\x1f\x7f]/.test(value) || !
|
|
19565
|
+
if (typeof value !== "string" || /[\x00-\x1f\x7f]/.test(value) || !isAbsolute3(value))
|
|
18787
19566
|
unavailable3();
|
|
18788
|
-
const result =
|
|
19567
|
+
const result = resolve6(value);
|
|
18789
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)))
|
|
18790
19569
|
unavailable3();
|
|
18791
19570
|
return result;
|
|
@@ -18814,28 +19593,28 @@ function prepare(options) {
|
|
|
18814
19593
|
function planApplicationAuthSynchronization(options) {
|
|
18815
19594
|
try {
|
|
18816
19595
|
const prepared = prepare(options);
|
|
18817
|
-
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 };
|
|
18818
19597
|
} catch {
|
|
18819
19598
|
return unavailable3();
|
|
18820
19599
|
}
|
|
18821
19600
|
}
|
|
18822
19601
|
function directory(filename2, privateState) {
|
|
18823
|
-
const parent =
|
|
18824
|
-
for (let ancestor = parent; ; ancestor =
|
|
18825
|
-
const metadata2 =
|
|
19602
|
+
const parent = dirname7(filename2);
|
|
19603
|
+
for (let ancestor = parent; ; ancestor = dirname7(ancestor)) {
|
|
19604
|
+
const metadata2 = lstatSync5(ancestor);
|
|
18826
19605
|
if (!metadata2.isDirectory() || metadata2.isSymbolicLink())
|
|
18827
19606
|
unavailable3();
|
|
18828
19607
|
if (ancestor === parse2(ancestor).root)
|
|
18829
19608
|
break;
|
|
18830
19609
|
}
|
|
18831
|
-
const metadata =
|
|
19610
|
+
const metadata = lstatSync5(parent);
|
|
18832
19611
|
if (process.platform !== "win32" && (metadata.uid !== process.getuid?.() || (metadata.mode & (privateState ? 63 : 18)) !== 0))
|
|
18833
19612
|
unavailable3();
|
|
18834
19613
|
}
|
|
18835
19614
|
function syncDirectory2(filename2) {
|
|
18836
19615
|
if (process.platform === "win32")
|
|
18837
19616
|
return;
|
|
18838
|
-
const descriptor2 = openSync2(
|
|
19617
|
+
const descriptor2 = openSync2(dirname7(filename2), constants2.O_RDONLY | constants2.O_DIRECTORY);
|
|
18839
19618
|
try {
|
|
18840
19619
|
fsyncSync2(descriptor2);
|
|
18841
19620
|
} finally {
|
|
@@ -18844,10 +19623,10 @@ function syncDirectory2(filename2) {
|
|
|
18844
19623
|
}
|
|
18845
19624
|
function currentPublicFile(filename2) {
|
|
18846
19625
|
try {
|
|
18847
|
-
const metadata =
|
|
19626
|
+
const metadata = lstatSync5(filename2);
|
|
18848
19627
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > 131072 || process.platform !== "win32" && (metadata.uid !== process.getuid?.() || (metadata.mode & 18) !== 0))
|
|
18849
19628
|
unavailable3();
|
|
18850
|
-
const bytes =
|
|
19629
|
+
const bytes = readFileSync6(filename2);
|
|
18851
19630
|
if (bytes.length > 131072)
|
|
18852
19631
|
unavailable3();
|
|
18853
19632
|
return new TextDecoder("utf8", { fatal: true }).decode(bytes);
|
|
@@ -18914,7 +19693,7 @@ async function synchronizeApplicationAuthTrustWithDigest(options) {
|
|
|
18914
19693
|
const lock = prepared.stateFile + ".delivery.lock";
|
|
18915
19694
|
const descriptor2 = openSync2(lock, "wx", 384);
|
|
18916
19695
|
try {
|
|
18917
|
-
|
|
19696
|
+
writeFileSync7(descriptor2, "backify-application-auth-delivery-v1");
|
|
18918
19697
|
fsyncSync2(descriptor2);
|
|
18919
19698
|
} finally {
|
|
18920
19699
|
closeSync2(descriptor2);
|
|
@@ -18925,19 +19704,19 @@ async function synchronizeApplicationAuthTrustWithDigest(options) {
|
|
|
18925
19704
|
const current = currentPublicFile(prepared.trustFile);
|
|
18926
19705
|
createPersistentApplicationAuthProvider(trust, { expectedScope: options.expectedScope, stateFile: prepared.stateFile, now: options.now });
|
|
18927
19706
|
const content = JSON.stringify(trust);
|
|
18928
|
-
const trustDigest =
|
|
19707
|
+
const trustDigest = createHash4("sha256").update(content).digest("hex");
|
|
18929
19708
|
if (current === content)
|
|
18930
19709
|
return { status: "unchanged", revision: trust.revision, validUntil: trust.validUntil, trustDigest };
|
|
18931
19710
|
writePending = true;
|
|
18932
|
-
const temporary = prepared.trustFile + "." +
|
|
19711
|
+
const temporary = prepared.trustFile + "." + randomUUID4() + ".tmp";
|
|
18933
19712
|
const output = openSync2(temporary, "wx", process.platform === "win32" ? 384 : 292);
|
|
18934
19713
|
try {
|
|
18935
|
-
|
|
19714
|
+
writeFileSync7(output, content);
|
|
18936
19715
|
fsyncSync2(output);
|
|
18937
19716
|
} finally {
|
|
18938
19717
|
closeSync2(output);
|
|
18939
19718
|
}
|
|
18940
|
-
|
|
19719
|
+
renameSync4(temporary, prepared.trustFile);
|
|
18941
19720
|
syncDirectory2(prepared.trustFile);
|
|
18942
19721
|
writePending = false;
|
|
18943
19722
|
return { status: "updated", revision: trust.revision, validUntil: trust.validUntil, trustDigest };
|
|
@@ -19514,16 +20293,16 @@ function jsonValue(value) {
|
|
|
19514
20293
|
|
|
19515
20294
|
// src/local-services.ts
|
|
19516
20295
|
import { spawn, execFile } from "node:child_process";
|
|
19517
|
-
import { createHash as
|
|
19518
|
-
import { createServer } from "node:net";
|
|
19519
|
-
import { existsSync as
|
|
19520
|
-
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";
|
|
19521
20300
|
|
|
19522
20301
|
// src/local-job-services.ts
|
|
19523
|
-
import { createHash as
|
|
19524
|
-
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";
|
|
19525
20304
|
import { isIP } from "node:net";
|
|
19526
|
-
import { dirname as
|
|
20305
|
+
import { dirname as dirname8, join as join6 } from "node:path";
|
|
19527
20306
|
function fields3(value, names) {
|
|
19528
20307
|
return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === names.length && names.every((name2) => Object.hasOwn(value, name2));
|
|
19529
20308
|
}
|
|
@@ -19574,12 +20353,12 @@ function validLocalJobState(value, otherIds) {
|
|
|
19574
20353
|
function prepareLocalJobService(hcrPath, retained, artifacts, applyMigrations) {
|
|
19575
20354
|
const requested = validateLocalJobArtifacts(artifacts);
|
|
19576
20355
|
const readSchema = (name2) => {
|
|
19577
|
-
const filename2 =
|
|
20356
|
+
const filename2 = join6(dirname8(hcrPath), "sql", name2), info = existsSync7(filename2) ? lstatSync6(filename2) : void 0;
|
|
19578
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");
|
|
19579
|
-
return
|
|
20358
|
+
return readFileSync7(filename2, "utf8");
|
|
19580
20359
|
};
|
|
19581
20360
|
const base = readSchema("jobs-v1.sql"), producer = readSchema("jobs-producer-v1.sql");
|
|
19582
|
-
const schemaDigests = { base:
|
|
20361
|
+
const schemaDigests = { base: createHash5("sha256").update(base).digest("hex"), producer: createHash5("sha256").update(producer).digest("hex") };
|
|
19583
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");
|
|
19584
20363
|
if (!retained?.initialized && !applyMigrations) throw new Error("Review both HCR job SQL assets and pass --apply-migrations for explicit native job schema approval");
|
|
19585
20364
|
const retainedArtifacts = retained ? validateLocalJobArtifacts(retained.artifacts) : [];
|
|
@@ -19588,20 +20367,20 @@ function prepareLocalJobService(hcrPath, retained, artifacts, applyMigrations) {
|
|
|
19588
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");
|
|
19589
20368
|
if (!previous) retainedArtifacts.push(artifact);
|
|
19590
20369
|
}
|
|
19591
|
-
const state = { ...retained ?? { id:
|
|
20370
|
+
const state = { ...retained ?? { id: randomUUID5(), password: randomBytes2(32).toString("hex"), initialized: false }, schemaDigests, artifacts: validateLocalJobArtifacts(retainedArtifacts) };
|
|
19592
20371
|
return { state, base, producer };
|
|
19593
20372
|
}
|
|
19594
20373
|
|
|
19595
20374
|
// src/local-services.ts
|
|
19596
20375
|
function writePrivate(filename2, value) {
|
|
19597
20376
|
if (isLink(filename2)) throw new Error("Local service files must not be symbolic links");
|
|
19598
|
-
const temporary = filename2 + "." +
|
|
19599
|
-
|
|
20377
|
+
const temporary = filename2 + "." + randomUUID6() + ".tmp";
|
|
20378
|
+
writeFileSync8(temporary, value, { flag: "wx", mode: 384 });
|
|
19600
20379
|
renameLocalFile(temporary, filename2);
|
|
19601
20380
|
}
|
|
19602
20381
|
function isLink(filename2) {
|
|
19603
20382
|
try {
|
|
19604
|
-
return
|
|
20383
|
+
return lstatSync7(filename2).isSymbolicLink();
|
|
19605
20384
|
} catch (error) {
|
|
19606
20385
|
if (error.code === "ENOENT") return false;
|
|
19607
20386
|
throw error;
|
|
@@ -19609,9 +20388,9 @@ function isLink(filename2) {
|
|
|
19609
20388
|
}
|
|
19610
20389
|
function assertLocalStatePath(root, filename2) {
|
|
19611
20390
|
const base = realpathSync(root);
|
|
19612
|
-
for (let current =
|
|
20391
|
+
for (let current = resolve7(filename2); current !== base; current = resolve7(current, "..")) {
|
|
19613
20392
|
const part = relative(base, current);
|
|
19614
|
-
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");
|
|
19615
20394
|
if (isLink(current)) throw new Error("Local service paths must not contain links");
|
|
19616
20395
|
}
|
|
19617
20396
|
}
|
|
@@ -19622,22 +20401,22 @@ function stateValid(value) {
|
|
|
19622
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);
|
|
19623
20402
|
}
|
|
19624
20403
|
function acquireLocalLease(root, relativePath2 = ".backify/dev/development.lock") {
|
|
19625
|
-
const filename2 =
|
|
20404
|
+
const filename2 = join7(root, relativePath2);
|
|
19626
20405
|
assertLocalStatePath(root, filename2);
|
|
19627
|
-
|
|
19628
|
-
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() });
|
|
19629
20408
|
try {
|
|
19630
|
-
|
|
20409
|
+
writeFileSync8(filename2, owner, { flag: "wx", mode: 384 });
|
|
19631
20410
|
} catch {
|
|
19632
20411
|
throw new Error("Local development already owns this repository, or a stale lifecycle lock requires explicit recovery");
|
|
19633
20412
|
}
|
|
19634
20413
|
return () => {
|
|
19635
|
-
if (
|
|
20414
|
+
if (existsSync8(filename2) && readFileSync8(filename2, "utf8") === owner) unlinkSync3(filename2);
|
|
19636
20415
|
};
|
|
19637
20416
|
}
|
|
19638
20417
|
async function allocateLocalPort(requested) {
|
|
19639
20418
|
if (requested !== void 0 && (!Number.isInteger(requested) || requested < 0 || requested > 65535)) throw new Error("Local service ports must be 0-65535");
|
|
19640
|
-
const server =
|
|
20419
|
+
const server = createServer2();
|
|
19641
20420
|
return new Promise((resolvePort, reject) => {
|
|
19642
20421
|
server.once("error", reject);
|
|
19643
20422
|
server.listen(requested ?? 0, "127.0.0.1", () => {
|
|
@@ -19653,7 +20432,7 @@ async function allocateLocalPort(requested) {
|
|
|
19653
20432
|
}
|
|
19654
20433
|
function command(executable, args2, options = {}) {
|
|
19655
20434
|
const failure2 = () => new Error("Local service command failed; verify installed tools, port availability and local state permissions");
|
|
19656
|
-
if (
|
|
20435
|
+
if (basename2(executable).replace(/\.exe$/i, "") === "pg_ctl") return new Promise((resolveOutput, reject) => {
|
|
19657
20436
|
const child = spawn(executable, args2, { ...options, windowsHide: true, stdio: "ignore" });
|
|
19658
20437
|
const timer = setTimeout(() => {
|
|
19659
20438
|
child.kill("SIGKILL");
|
|
@@ -19675,29 +20454,29 @@ function command(executable, args2, options = {}) {
|
|
|
19675
20454
|
});
|
|
19676
20455
|
}
|
|
19677
20456
|
function postgresExecutable(name2, bin) {
|
|
19678
|
-
const directory2 = bin ?? process.env.BACKIFY_LOCAL_POSTGRES_BIN ?? (process.platform === "win32" ?
|
|
19679
|
-
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;
|
|
19680
20459
|
}
|
|
19681
20460
|
function localMigrationFiles(root, directory2) {
|
|
19682
20461
|
if (directory2 === void 0) return [];
|
|
19683
20462
|
if (typeof directory2 !== "string") throw new Error("Local migrations require a repository-relative directory");
|
|
19684
|
-
const folder =
|
|
20463
|
+
const folder = resolve7(root, directory2);
|
|
19685
20464
|
assertLocalStatePath(root, folder);
|
|
19686
|
-
if (!
|
|
19687
|
-
const entries =
|
|
20465
|
+
if (!existsSync8(folder)) throw new Error("Declared migration directory does not exist");
|
|
20466
|
+
const entries = readdirSync5(folder);
|
|
19688
20467
|
const filenames = entries.filter((entry) => entry.endsWith(".sql")).sort();
|
|
19689
20468
|
if (filenames.length > 100 || entries.some((entry) => !entry.endsWith(".sql"))) throw new Error("Migrations require at most 100 flat .sql files");
|
|
19690
20469
|
let total = 0;
|
|
19691
20470
|
return filenames.map((name2) => {
|
|
19692
20471
|
if (!/^[A-Za-z0-9_.-]{1,150}\.sql$/.test(name2)) throw new Error("Invalid migration filename");
|
|
19693
|
-
const filename2 =
|
|
20472
|
+
const filename2 = join7(folder, name2);
|
|
19694
20473
|
assertLocalStatePath(root, filename2);
|
|
19695
|
-
const info =
|
|
20474
|
+
const info = lstatSync7(filename2);
|
|
19696
20475
|
if (!info.isFile() || info.size > 1024 * 1024 || (total += info.size) > 4 * 1024 * 1024) throw new Error("Migration SQL exceeds supported limits");
|
|
19697
|
-
const sql =
|
|
20476
|
+
const sql = readFileSync8(filename2, "utf8");
|
|
19698
20477
|
if (!sql.trim()) throw new Error("Migration SQL must not be empty");
|
|
19699
20478
|
validateLocalMigrationSql(sql);
|
|
19700
|
-
return { name: name2, digest:
|
|
20479
|
+
return { name: name2, digest: createHash6("sha256").update(sql).digest("hex"), sql };
|
|
19701
20480
|
});
|
|
19702
20481
|
}
|
|
19703
20482
|
function validateLocalMigrationSql(sql) {
|
|
@@ -19771,19 +20550,19 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19771
20550
|
const reviewedScope = options.jobIdentityScope;
|
|
19772
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");
|
|
19773
20552
|
const installed = options.hcrPath || process.env.BACKIFY_LOCAL_HCR_PATH ? void 0 : installedLocalRuntime();
|
|
19774
|
-
const hcrPath =
|
|
19775
|
-
if (!
|
|
19776
|
-
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");
|
|
19777
20556
|
assertLocalStatePath(root, directory2);
|
|
19778
|
-
|
|
20557
|
+
mkdirSync4(directory2, { recursive: true, mode: 448 });
|
|
19779
20558
|
assertLocalStatePath(root, directory2);
|
|
19780
|
-
const statePath =
|
|
20559
|
+
const statePath = join7(directory2, "host.json");
|
|
19781
20560
|
assertLocalStatePath(root, statePath);
|
|
19782
20561
|
let existing;
|
|
19783
|
-
if (
|
|
19784
|
-
if (
|
|
20562
|
+
if (existsSync8(statePath)) {
|
|
20563
|
+
if (lstatSync7(statePath).size > 16384) throw new Error("Local ownership state exceeds supported limits");
|
|
19785
20564
|
try {
|
|
19786
|
-
existing = JSON.parse(
|
|
20565
|
+
existing = JSON.parse(readFileSync8(statePath, "utf8"));
|
|
19787
20566
|
} catch {
|
|
19788
20567
|
throw new Error("Invalid local service ownership state; preserve the data and repair explicitly");
|
|
19789
20568
|
}
|
|
@@ -19791,31 +20570,31 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19791
20570
|
}
|
|
19792
20571
|
const state = existing ?? {
|
|
19793
20572
|
version: 1,
|
|
19794
|
-
scope: reviewedScope ? structuredClone(reviewedScope) : { organizationId:
|
|
19795
|
-
databaseId:
|
|
19796
|
-
storageId:
|
|
20573
|
+
scope: reviewedScope ? structuredClone(reviewedScope) : { organizationId: randomUUID6(), applicationId: randomUUID6(), environmentId: randomUUID6() },
|
|
20574
|
+
databaseId: randomUUID6(),
|
|
20575
|
+
storageId: randomUUID6(),
|
|
19797
20576
|
configurationVersion: 0,
|
|
19798
|
-
adminPassword:
|
|
19799
|
-
databasePassword:
|
|
20577
|
+
adminPassword: randomBytes3(32).toString("hex"),
|
|
20578
|
+
databasePassword: randomBytes3(32).toString("hex")
|
|
19800
20579
|
};
|
|
19801
20580
|
if (!stateValid(state)) throw new Error("Invalid local service ownership state; preserve the data and repair explicitly");
|
|
19802
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");
|
|
19803
20582
|
let eventSchema;
|
|
19804
20583
|
if (events) {
|
|
19805
|
-
const filename2 =
|
|
19806
|
-
if (!
|
|
19807
|
-
eventSchema =
|
|
19808
|
-
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");
|
|
19809
20588
|
if (state.events && state.events.schemaDigest !== digest4) throw new Error("Native event schema changed; preserve its database and plan an explicit migration.");
|
|
19810
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.");
|
|
19811
|
-
state.events ??= { id:
|
|
20590
|
+
state.events ??= { id: randomUUID6(), password: randomBytes3(32).toString("hex"), schemaDigest: digest4, initialized: false };
|
|
19812
20591
|
}
|
|
19813
20592
|
const jobSchema = jobs ? prepareLocalJobService(hcrPath, state.jobs, options.jobArtifacts, options.applyMigrations) : void 0;
|
|
19814
20593
|
if (jobSchema) state.jobs = jobSchema.state;
|
|
19815
20594
|
const storageNames = manifest.capabilities.storage?.buckets === void 0 ? ["default"] : manifest.capabilities.storage.buckets.map((bucket) => bucket.name);
|
|
19816
20595
|
state.storageBindings ??= { default: state.storageId };
|
|
19817
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");
|
|
19818
|
-
for (const name2 of storageNames) state.storageBindings[name2] ??=
|
|
20597
|
+
for (const name2 of storageNames) state.storageBindings[name2] ??= randomUUID6();
|
|
19819
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");
|
|
19820
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");
|
|
19821
20600
|
const migrations = database ? localMigrationFiles(root, manifest.capabilities.database?.migrations) : [];
|
|
@@ -19823,9 +20602,9 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19823
20602
|
const databasePort = database || events || jobs ? await allocateLocalPort(options.databasePort) : void 0;
|
|
19824
20603
|
if (databasePort !== void 0) state.retainedDatabasePort = databasePort;
|
|
19825
20604
|
if (databasePort === hcrPort) throw new Error("HCR and PostgreSQL require separate ports");
|
|
19826
|
-
const cluster =
|
|
20605
|
+
const cluster = join7(directory2, "postgres");
|
|
19827
20606
|
assertLocalStatePath(root, cluster);
|
|
19828
|
-
const
|
|
20607
|
+
const release2 = acquireLocalLease(root, ".backify/dev/services/lifecycle.lock");
|
|
19829
20608
|
let postgresStarted = false;
|
|
19830
20609
|
let hcr;
|
|
19831
20610
|
let timer;
|
|
@@ -19839,7 +20618,7 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19839
20618
|
}
|
|
19840
20619
|
};
|
|
19841
20620
|
const runtimeUrl = "http://127.0.0.1:" + hcrPort;
|
|
19842
|
-
const tokenPath =
|
|
20621
|
+
const tokenPath = join7(directory2, "workload-token");
|
|
19843
20622
|
const stop = () => stopping ??= (async () => {
|
|
19844
20623
|
if (timer) clearInterval(timer);
|
|
19845
20624
|
if (snapshotTimer) clearInterval(snapshotTimer);
|
|
@@ -19858,11 +20637,11 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19858
20637
|
}
|
|
19859
20638
|
notify("hcr", "stopped");
|
|
19860
20639
|
try {
|
|
19861
|
-
if (postgresStarted &&
|
|
20640
|
+
if (postgresStarted && existsSync8(join7(cluster, "postmaster.pid"))) await command(postgresExecutable("pg_ctl", options.postgresBin), ["-D", cluster, "-m", "fast", "-w", "stop"]);
|
|
19862
20641
|
if (postgresStarted) notify("postgres", "stopped");
|
|
19863
|
-
if (
|
|
20642
|
+
if (existsSync8(tokenPath)) unlinkSync3(tokenPath);
|
|
19864
20643
|
} finally {
|
|
19865
|
-
|
|
20644
|
+
release2();
|
|
19866
20645
|
}
|
|
19867
20646
|
})();
|
|
19868
20647
|
const applied = [];
|
|
@@ -19870,28 +20649,28 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19870
20649
|
writePrivate(statePath, JSON.stringify(state));
|
|
19871
20650
|
if (database || events || jobs) {
|
|
19872
20651
|
notify("postgres", "starting");
|
|
19873
|
-
if (!
|
|
19874
|
-
const passwordPath =
|
|
20652
|
+
if (!existsSync8(join7(cluster, "PG_VERSION"))) {
|
|
20653
|
+
const passwordPath = join7(directory2, "init-password");
|
|
19875
20654
|
writePrivate(passwordPath, state.adminPassword);
|
|
19876
20655
|
try {
|
|
19877
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"]);
|
|
19878
20657
|
} finally {
|
|
19879
|
-
if (
|
|
20658
|
+
if (existsSync8(passwordPath)) unlinkSync3(passwordPath);
|
|
19880
20659
|
}
|
|
19881
20660
|
}
|
|
19882
|
-
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");
|
|
19883
20662
|
postgresStarted = true;
|
|
19884
|
-
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"]);
|
|
19885
20664
|
notify("postgres", "ready");
|
|
19886
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 } });
|
|
19887
20666
|
if (database) {
|
|
19888
20667
|
if ((await sql("SELECT count(*) FROM pg_roles WHERE rolname='backify_local'")).trim() === "0") {
|
|
19889
|
-
const filename2 =
|
|
20668
|
+
const filename2 = join7(directory2, "role-" + randomUUID6() + ".sql");
|
|
19890
20669
|
writePrivate(filename2, "CREATE ROLE backify_local LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '" + state.databasePassword + "';");
|
|
19891
20670
|
try {
|
|
19892
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 } });
|
|
19893
20672
|
} finally {
|
|
19894
|
-
if (
|
|
20673
|
+
if (existsSync8(filename2)) unlinkSync3(filename2);
|
|
19895
20674
|
}
|
|
19896
20675
|
}
|
|
19897
20676
|
if ((await sql("SELECT count(*) FROM pg_database WHERE datname='backify_local'")).trim() === "0") await sql("CREATE DATABASE backify_local OWNER backify_local");
|
|
@@ -19906,12 +20685,12 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19906
20685
|
notify("migrations", "starting");
|
|
19907
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);
|
|
19908
20687
|
for (const migration of pending) {
|
|
19909
|
-
const filename2 =
|
|
20688
|
+
const filename2 = join7(directory2, "apply-" + randomUUID6() + ".sql");
|
|
19910
20689
|
writePrivate(filename2, "BEGIN;\n" + migration.sql + "\n;\nINSERT INTO public.__backify_migrations(name,digest) VALUES ('" + migration.name + "','" + migration.digest + "');\nCOMMIT;\n");
|
|
19911
20690
|
try {
|
|
19912
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 } });
|
|
19913
20692
|
} finally {
|
|
19914
|
-
if (
|
|
20693
|
+
if (existsSync8(filename2)) unlinkSync3(filename2);
|
|
19915
20694
|
}
|
|
19916
20695
|
applied.push(migration.name);
|
|
19917
20696
|
}
|
|
@@ -19922,20 +20701,20 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19922
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 } });
|
|
19923
20702
|
if (!eventState.initialized) {
|
|
19924
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.");
|
|
19925
|
-
const roleFile =
|
|
20704
|
+
const roleFile = join7(directory2, "event-role-" + randomUUID6() + ".sql");
|
|
19926
20705
|
writePrivate(roleFile, "CREATE ROLE backify_events LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '" + eventState.password + "';");
|
|
19927
20706
|
try {
|
|
19928
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 } });
|
|
19929
20708
|
} finally {
|
|
19930
|
-
if (
|
|
20709
|
+
if (existsSync8(roleFile)) unlinkSync3(roleFile);
|
|
19931
20710
|
}
|
|
19932
20711
|
await sql("CREATE DATABASE backify_events OWNER backify_events");
|
|
19933
|
-
const schemaFile =
|
|
20712
|
+
const schemaFile = join7(directory2, "event-schema-" + randomUUID6() + ".sql");
|
|
19934
20713
|
writePrivate(schemaFile, eventSchema);
|
|
19935
20714
|
try {
|
|
19936
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 } });
|
|
19937
20716
|
} finally {
|
|
19938
|
-
if (
|
|
20717
|
+
if (existsSync8(schemaFile)) unlinkSync3(schemaFile);
|
|
19939
20718
|
}
|
|
19940
20719
|
eventState.initialized = true;
|
|
19941
20720
|
writePrivate(statePath, JSON.stringify(state));
|
|
@@ -19949,21 +20728,21 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19949
20728
|
const adminSql = (query, databaseName = "postgres") => jobCommand("backify_admin", state.adminPassword, databaseName, ["-c", query]);
|
|
19950
20729
|
if (!jobState.initialized) {
|
|
19951
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");
|
|
19952
|
-
const roleFile =
|
|
20731
|
+
const roleFile = join7(directory2, "job-role-" + randomUUID6() + ".sql");
|
|
19953
20732
|
writePrivate(roleFile, "CREATE ROLE backify_jobs LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD '" + jobState.password + "';");
|
|
19954
20733
|
try {
|
|
19955
20734
|
await jobCommand("backify_admin", state.adminPassword, "postgres", ["-f", roleFile]);
|
|
19956
20735
|
} finally {
|
|
19957
|
-
if (
|
|
20736
|
+
if (existsSync8(roleFile)) unlinkSync3(roleFile);
|
|
19958
20737
|
}
|
|
19959
20738
|
await adminSql("CREATE DATABASE backify_jobs OWNER backify_admin");
|
|
19960
20739
|
for (const schema of [jobSchema.base, jobSchema.producer]) {
|
|
19961
|
-
const filename2 =
|
|
20740
|
+
const filename2 = join7(directory2, "job-schema-" + randomUUID6() + ".sql");
|
|
19962
20741
|
writePrivate(filename2, schema);
|
|
19963
20742
|
try {
|
|
19964
20743
|
await jobCommand("backify_admin", state.adminPassword, "backify_jobs", ["-f", filename2]);
|
|
19965
20744
|
} finally {
|
|
19966
|
-
if (
|
|
20745
|
+
if (existsSync8(filename2)) unlinkSync3(filename2);
|
|
19967
20746
|
}
|
|
19968
20747
|
}
|
|
19969
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");
|
|
@@ -19992,7 +20771,7 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
19992
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" }] : [],
|
|
19993
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" }] : []
|
|
19994
20773
|
];
|
|
19995
|
-
const snapshotPath =
|
|
20774
|
+
const snapshotPath = join7(directory2, "snapshot.json");
|
|
19996
20775
|
const writeSnapshot = () => writePrivate(snapshotPath, JSON.stringify({ schemaVersion: 1, configurationVersion: state.configurationVersion, expiresAt: new Date(Date.now() + 7 * 864e5).toISOString(), bindings }));
|
|
19997
20776
|
const renewConfiguration = () => {
|
|
19998
20777
|
if (stopping) throw new Error("Stopped local services cannot renew configuration");
|
|
@@ -20002,15 +20781,15 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
20002
20781
|
writeSnapshot();
|
|
20003
20782
|
};
|
|
20004
20783
|
writeSnapshot();
|
|
20005
|
-
const settingsPath =
|
|
20784
|
+
const settingsPath = join7(directory2, "appsettings.json");
|
|
20006
20785
|
writePrivate(settingsPath, JSON.stringify({ Hcr: {
|
|
20007
20786
|
Authentication: { PublicKeyPem: publicKey.export({ type: "spki", format: "pem" }), Issuer: "backify-local", Audience: "backify-local-hcr" },
|
|
20008
20787
|
SnapshotPath: snapshotPath,
|
|
20009
|
-
SnapshotJournalPath:
|
|
20788
|
+
SnapshotJournalPath: join7(directory2, "journal.json"),
|
|
20010
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 } } : {} },
|
|
20011
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 } } : {} },
|
|
20012
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 } } : {} },
|
|
20013
|
-
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) }])) }
|
|
20014
20793
|
} }));
|
|
20015
20794
|
issueToken();
|
|
20016
20795
|
notify("hcr", "starting");
|
|
@@ -20024,7 +20803,7 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
20024
20803
|
while (true) {
|
|
20025
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");
|
|
20026
20805
|
try {
|
|
20027
|
-
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" });
|
|
20028
20807
|
if (response.ok) {
|
|
20029
20808
|
const status = await response.json();
|
|
20030
20809
|
if (status.status === "ready" && status.configurationVersion === state.configurationVersion) break;
|
|
@@ -20033,8 +20812,8 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
20033
20812
|
}
|
|
20034
20813
|
await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
|
|
20035
20814
|
}
|
|
20036
|
-
if (events) await createHcrEventsProvider({ runtimeUrl, token: () =>
|
|
20037
|
-
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();
|
|
20038
20817
|
timer = setInterval(() => {
|
|
20039
20818
|
try {
|
|
20040
20819
|
issueToken();
|
|
@@ -20050,7 +20829,7 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
20050
20829
|
}
|
|
20051
20830
|
}, 36e5);
|
|
20052
20831
|
notify("hcr", "ready");
|
|
20053
|
-
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");
|
|
20054
20833
|
return { identityScope: { ...state.scope }, runtimeUrl, tokenPath, database, storage, events, jobs, databasePort, migrations: applied, configurationFingerprint, renewConfiguration, stop };
|
|
20055
20834
|
} catch (error) {
|
|
20056
20835
|
await stop();
|
|
@@ -20059,29 +20838,29 @@ async function startLocalServices(root, manifest, options = {}) {
|
|
|
20059
20838
|
}
|
|
20060
20839
|
|
|
20061
20840
|
// src/local-job-auth.ts
|
|
20062
|
-
import { createHash as
|
|
20063
|
-
import { lstatSync as
|
|
20064
|
-
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";
|
|
20065
20844
|
function prepareLocalJobAuth(root, filename2) {
|
|
20066
20845
|
const read = (path, maximum) => {
|
|
20067
20846
|
assertLocalStatePath(root, path);
|
|
20068
|
-
const metadata =
|
|
20847
|
+
const metadata = lstatSync8(path);
|
|
20069
20848
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > maximum) throw new Error();
|
|
20070
|
-
const bytes =
|
|
20849
|
+
const bytes = readFileSync9(path);
|
|
20071
20850
|
if (bytes.length > maximum) throw new Error();
|
|
20072
20851
|
return bytes;
|
|
20073
20852
|
};
|
|
20074
20853
|
try {
|
|
20075
|
-
if (!filename2 ||
|
|
20076
|
-
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)));
|
|
20077
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();
|
|
20078
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();
|
|
20079
|
-
if ([input.trustFile, input.stateFile].some((path) => typeof path !== "string" || !path ||
|
|
20080
|
-
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);
|
|
20081
20860
|
if (trustFile === stateFile) throw new Error();
|
|
20082
20861
|
const bytes = read(trustFile, 131072), trust = JSON.parse(new TextDecoder("utf8", { fatal: true }).decode(bytes));
|
|
20083
20862
|
read(stateFile, 2048);
|
|
20084
|
-
if (
|
|
20863
|
+
if (createHash7("sha256").update(bytes).digest("hex") !== input.trustDigest || trust.revision !== input.revision) throw new Error();
|
|
20085
20864
|
createPersistentApplicationAuthProvider(trust, { expectedScope: input.scope, stateFile });
|
|
20086
20865
|
return { trustFile, stateFile, scope: structuredClone(input.scope), revision: input.revision, trustDigest: input.trustDigest };
|
|
20087
20866
|
} catch {
|
|
@@ -20090,39 +20869,39 @@ function prepareLocalJobAuth(root, filename2) {
|
|
|
20090
20869
|
}
|
|
20091
20870
|
|
|
20092
20871
|
// src/local-job-trust-renewal.ts
|
|
20093
|
-
import { createHash as
|
|
20094
|
-
import { lstatSync as
|
|
20095
|
-
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";
|
|
20096
20875
|
|
|
20097
20876
|
// src/workload-trust-sync.ts
|
|
20098
|
-
import { lstatSync as
|
|
20099
|
-
import { isAbsolute as
|
|
20100
|
-
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";
|
|
20101
20880
|
function prepareWorkloadTrustSynchronization(filename2) {
|
|
20102
20881
|
try {
|
|
20103
|
-
if (!
|
|
20104
|
-
const metadata =
|
|
20882
|
+
if (!isAbsolute6(filename2)) throw new Error();
|
|
20883
|
+
const metadata = lstatSync9(filename2);
|
|
20105
20884
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > 65536) throw new Error();
|
|
20106
|
-
const bytes =
|
|
20885
|
+
const bytes = readFileSync10(filename2);
|
|
20107
20886
|
if (bytes.length > 65536) throw new Error();
|
|
20108
20887
|
const value = JSON.parse(new TextDecoder("utf8", { fatal: true }).decode(bytes));
|
|
20109
20888
|
const allowed = ["controlPlaneUrl", "agentId", "expectedScope", "managementIssuer", "managementAudience", "managementPublicKeyPem", "credentialFile", "trustFile", "stateFile", "intervalSeconds"];
|
|
20110
|
-
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((name2) => !allowed.includes(name2)) || typeof value.credentialFile !== "string" || !
|
|
20111
|
-
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);
|
|
20112
20891
|
const comparison = (filename3) => process.platform === "win32" ? filename3.toLowerCase() : filename3;
|
|
20113
20892
|
if ([trustFile, stateFile, stateFile + ".lock", stateFile + ".delivery.lock"].map(comparison).includes(comparison(credentialFile))) throw new Error();
|
|
20114
20893
|
const intervalSeconds = value.intervalSeconds ?? 30;
|
|
20115
20894
|
if (!Number.isSafeInteger(intervalSeconds) || intervalSeconds < 10 || intervalSeconds > 300) throw new Error();
|
|
20116
20895
|
const credential = () => {
|
|
20117
|
-
const metadata2 =
|
|
20896
|
+
const metadata2 = lstatSync9(credentialFile);
|
|
20118
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");
|
|
20119
|
-
const bytes2 =
|
|
20898
|
+
const bytes2 = readFileSync10(credentialFile);
|
|
20120
20899
|
if (bytes2.length > 256) throw new Error("Management credential unavailable");
|
|
20121
20900
|
return new TextDecoder("utf8", { fatal: true }).decode(bytes2).trim();
|
|
20122
20901
|
};
|
|
20123
20902
|
const options = { controlPlaneUrl: value.controlPlaneUrl, agentId: value.agentId, expectedScope: value.expectedScope, managementIssuer: value.managementIssuer, managementAudience: value.managementAudience, managementPublicKeyPem: value.managementPublicKeyPem, credential, trustFile, stateFile };
|
|
20124
20903
|
const plan = planApplicationAuthSynchronization(options);
|
|
20125
|
-
return { options, plan, intervalSeconds, credentialFile, configurationDigest:
|
|
20904
|
+
return { options, plan, intervalSeconds, credentialFile, configurationDigest: createHash8("sha256").update(bytes).digest("hex") };
|
|
20126
20905
|
} catch {
|
|
20127
20906
|
throw new Error("Invalid operator trust-sync configuration");
|
|
20128
20907
|
}
|
|
@@ -20173,22 +20952,22 @@ async function workloadTrustSynchronization(args2, settings = {}) {
|
|
|
20173
20952
|
// src/local-job-trust-renewal.ts
|
|
20174
20953
|
function prepareLocalJobTrustRenewal(root, filename2, mount) {
|
|
20175
20954
|
try {
|
|
20176
|
-
if (!filename2 ||
|
|
20177
|
-
const configurationFile =
|
|
20955
|
+
if (!filename2 || isAbsolute7(filename2)) throw new Error();
|
|
20956
|
+
const configurationFile = resolve10(root, filename2);
|
|
20178
20957
|
assertLocalStatePath(root, configurationFile);
|
|
20179
20958
|
const configured = prepareWorkloadTrustSynchronization(configurationFile);
|
|
20180
20959
|
const { credential, ...options } = configured.options;
|
|
20181
20960
|
const same = (first, second) => process.platform === "win32" ? first.toLowerCase() === second.toLowerCase() : first === second;
|
|
20182
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();
|
|
20183
20962
|
assertLocalStatePath(root, configured.credentialFile);
|
|
20184
|
-
const metadata =
|
|
20963
|
+
const metadata = lstatSync10(configured.credentialFile);
|
|
20185
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();
|
|
20186
20965
|
return { configurationFile, configurationDigest: configured.configurationDigest, credentialFile: configured.credentialFile, intervalSeconds: configured.intervalSeconds, options };
|
|
20187
20966
|
} catch {
|
|
20188
20967
|
throw new Error("Invalid reviewed native worker trust renewal; preserve trust and state and review explicitly");
|
|
20189
20968
|
}
|
|
20190
20969
|
}
|
|
20191
|
-
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 }) {
|
|
20192
20971
|
const { createHash: createHash16, lstatSync: lstatSync20, readFileSync: readFileSync25, isAbsolute: isAbsolute17, relative: relative11, resolve: resolve24, createPersistentApplicationAuthProvider: createPersistentApplicationAuthProvider2, synchronizeApplicationAuthTrustWithDigest: synchronizeApplicationAuthTrustWithDigest2 } = dependencies;
|
|
20193
20972
|
const key = (filename2) => process.platform === "win32" ? resolve24(filename2).toLowerCase() : resolve24(filename2);
|
|
20194
20973
|
const groups = [];
|
|
@@ -20287,34 +21066,34 @@ function applyLocalJobTrustRenewal(workers, updates, check) {
|
|
|
20287
21066
|
|
|
20288
21067
|
// src/local-job-executor.ts
|
|
20289
21068
|
function prepareLocalJobWorker(root, directory2, artifacts, manifest, authFile, trustSyncFile) {
|
|
20290
|
-
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");
|
|
20291
21070
|
const authenticationEnabled = !!manifest.capabilities.auth && manifest.capabilities.auth.enabled !== false;
|
|
20292
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");
|
|
20293
21072
|
if (!authenticationEnabled && authFile !== void 0) throw new Error("Native worker trust mounts require an enabled auth capability");
|
|
20294
21073
|
if (trustSyncFile !== void 0 && authFile === void 0) throw new Error("Native worker trust renewal requires explicit reviewed auth mounts");
|
|
20295
21074
|
const authTrust = authFile === void 0 ? void 0 : prepareLocalJobAuth(root, authFile);
|
|
20296
21075
|
if (authTrust && trustSyncFile !== void 0) authTrust.renewal = prepareLocalJobTrustRenewal(root, trustSyncFile, authTrust);
|
|
20297
|
-
const workerFile =
|
|
21076
|
+
const workerFile = join8(resolve11(root, directory2), "workload.mjs"), metadataFile = join8(resolve11(root, directory2), "artifact.json");
|
|
20298
21077
|
for (const filename2 of [workerFile, metadataFile]) assertLocalStatePath(root, filename2);
|
|
20299
|
-
if (!
|
|
20300
|
-
const metadata = JSON.parse(
|
|
20301
|
-
const bundle =
|
|
20302
|
-
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");
|
|
20303
21082
|
const scheduleDiscovery = bundle.subarray(0, 100).toString("utf8").startsWith('var __backifyWorkerProtocols = ["hcr-v1","hcr-schedules-v1"];\n');
|
|
20304
|
-
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");
|
|
20305
21084
|
const artifact = validateLocalJobArtifacts(artifacts).find((item) => item.artifactVersion === actual);
|
|
20306
21085
|
if (!artifact) throw new Error("Native worker bundle is not an explicitly reviewed job artifact");
|
|
20307
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");
|
|
20308
21087
|
return { root, workerFile, artifact, schedulerEnabled: !!manifest.capabilities.scheduler && manifest.capabilities.scheduler.enabled !== false, ...scheduleDiscovery ? { scheduleDiscovery: true } : {}, ...authTrust ? { authTrust } : {} };
|
|
20309
21088
|
}
|
|
20310
21089
|
function readLocalJobArtifacts(root, filename2) {
|
|
20311
|
-
if (!filename2 ||
|
|
20312
|
-
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);
|
|
20313
21092
|
assertLocalStatePath(root, path);
|
|
20314
|
-
if (!
|
|
20315
|
-
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")));
|
|
20316
21095
|
}
|
|
20317
|
-
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 }) {
|
|
20318
21097
|
const { spawn: spawn6, createHash: createHash16, lstatSync: lstatSync20, readFileSync: readFileSync25, isAbsolute: isAbsolute17, relative: relative11, resolve: resolve24, validateWorkloadJobSchedules: validateWorkloadJobSchedules2, createPersistentApplicationAuthProvider: createPersistentApplicationAuthProvider2 } = dependencies;
|
|
20319
21098
|
const assertReady = () => {
|
|
20320
21099
|
for (let current = resolve24(options.workerFile); current !== options.root; current = resolve24(current, "..")) {
|
|
@@ -20447,9 +21226,9 @@ function createLocalJobExecutor(options, dependencies = { spawn: spawn2, createH
|
|
|
20447
21226
|
}
|
|
20448
21227
|
|
|
20449
21228
|
// src/local-native-jobs.ts
|
|
20450
|
-
import { createHash as
|
|
21229
|
+
import { createHash as createHash11 } from "node:crypto";
|
|
20451
21230
|
function createLocalNativeJobs(local, native, executor, artifact, scope, onFailure = () => {
|
|
20452
|
-
}, hash2 =
|
|
21231
|
+
}, hash2 = createHash11, catalog = { workloadJobSchedules, validateWorkloadJobSchedules, HcrJobsRequestError }, historical = []) {
|
|
20453
21232
|
const workers = [{ artifact, executor }, ...historical];
|
|
20454
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");
|
|
20455
21234
|
const lanes = workers.flatMap((worker) => worker.artifact.jobs.map((policy) => ({ ...worker, policy })));
|
|
@@ -20707,9 +21486,9 @@ process.on("SIGTERM", stop); process.on("SIGINT", stop); process.once("disconnec
|
|
|
20707
21486
|
|
|
20708
21487
|
// src/local-frontend.ts
|
|
20709
21488
|
import { spawn as spawn3 } from "node:child_process";
|
|
20710
|
-
import { readFileSync as
|
|
21489
|
+
import { readFileSync as readFileSync14, realpathSync as realpathSync3, existsSync as existsSync9, statSync } from "node:fs";
|
|
20711
21490
|
import { createRequire as createRequire3 } from "node:module";
|
|
20712
|
-
import { dirname as
|
|
21491
|
+
import { dirname as dirname10, isAbsolute as isAbsolute10, join as join10, relative as relative5, resolve as resolve13 } from "node:path";
|
|
20713
21492
|
import { pathToFileURL } from "node:url";
|
|
20714
21493
|
|
|
20715
21494
|
// src/local-frontend-guard.ts
|
|
@@ -20776,24 +21555,24 @@ await server.listen();
|
|
|
20776
21555
|
`;
|
|
20777
21556
|
|
|
20778
21557
|
// src/local-static-frontend.ts
|
|
20779
|
-
import { createServer as
|
|
20780
|
-
import { lstatSync as
|
|
20781
|
-
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";
|
|
20782
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" };
|
|
20783
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()));
|
|
20784
21563
|
function publicFile(directory2, pathname) {
|
|
20785
21564
|
const parts = pathname.split("/").slice(1);
|
|
20786
21565
|
if (parts.at(-1) === "") parts[parts.length - 1] = "index.html";
|
|
20787
21566
|
if (!publicParts(parts)) throw new Error("refused");
|
|
20788
|
-
const filename2 =
|
|
20789
|
-
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");
|
|
20790
21569
|
let current = directory2;
|
|
20791
|
-
if (
|
|
21570
|
+
if (lstatSync12(current).isSymbolicLink() || realpathSync2(current) !== directory2) throw new Error("refused");
|
|
20792
21571
|
for (const segment of parts) {
|
|
20793
|
-
current =
|
|
20794
|
-
if (
|
|
21572
|
+
current = join9(current, segment);
|
|
21573
|
+
if (lstatSync12(current).isSymbolicLink()) throw new Error("refused");
|
|
20795
21574
|
}
|
|
20796
|
-
const stat =
|
|
21575
|
+
const stat = lstatSync12(filename2);
|
|
20797
21576
|
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 5242880) throw new Error("refused");
|
|
20798
21577
|
return filename2;
|
|
20799
21578
|
}
|
|
@@ -20808,7 +21587,7 @@ async function startLocalStaticFrontend(directory2, port, backendUrl) {
|
|
|
20808
21587
|
const watcher = watch(directory2, { recursive: true }, (_, filename2) => {
|
|
20809
21588
|
if (filename2 && publicParts(filename2.toString().split(/[\\/]/)) && contentTypes[extname2(filename2.toString()).toLowerCase()]) revision++;
|
|
20810
21589
|
});
|
|
20811
|
-
const server =
|
|
21590
|
+
const server = createServer3((request, response) => {
|
|
20812
21591
|
const send = (status, type, body) => {
|
|
20813
21592
|
response.writeHead(status, { "content-type": type, "cache-control": "no-store", "x-content-type-options": "nosniff", "content-length": Buffer.byteLength(body) });
|
|
20814
21593
|
response.end(request.method === "HEAD" ? void 0 : body);
|
|
@@ -20821,7 +21600,7 @@ async function startLocalStaticFrontend(directory2, port, backendUrl) {
|
|
|
20821
21600
|
if (pathname === "/__backify/revision") return send(200, "text/plain", String(revision));
|
|
20822
21601
|
if (pathname === "/__backify/config.json") return send(200, "application/json", JSON.stringify({ backendUrl: backendUrl ?? null }));
|
|
20823
21602
|
const filename2 = publicFile(directory2, pathname);
|
|
20824
|
-
const content =
|
|
21603
|
+
const content = readFileSync13(filename2);
|
|
20825
21604
|
if (content.length > 5242880) throw new Error("refused");
|
|
20826
21605
|
const extension = extname2(filename2).toLowerCase();
|
|
20827
21606
|
const body = extension === ".html" ? content.toString("utf8") + '\n<script src="/__backify/reload.js?revision=' + revision + '"></script>\n' : content;
|
|
@@ -20864,31 +21643,31 @@ function prepareLocalFrontend(root, options, privateFiles = []) {
|
|
|
20864
21643
|
if (options.frontendPort !== void 0) throw new Error("Frontend port requires an explicit frontend directory");
|
|
20865
21644
|
return void 0;
|
|
20866
21645
|
}
|
|
20867
|
-
const directory2 = realpathSync3(
|
|
21646
|
+
const directory2 = realpathSync3(resolve13(root, options.frontend));
|
|
20868
21647
|
const part = relative5(realpathSync3(root), directory2);
|
|
20869
|
-
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");
|
|
20870
21649
|
if (options.frontendAdapter === "static") {
|
|
20871
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");
|
|
20872
21651
|
return prepareLocalStaticFrontend(directory2);
|
|
20873
21652
|
}
|
|
20874
|
-
if (!
|
|
20875
|
-
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"));
|
|
20876
21655
|
let packagePath;
|
|
20877
21656
|
try {
|
|
20878
21657
|
packagePath = require3.resolve("vite/package.json");
|
|
20879
21658
|
} catch {
|
|
20880
21659
|
throw new Error("Frontend requires locally installed Vite; Backify does not install dependencies automatically");
|
|
20881
21660
|
}
|
|
20882
|
-
const metadata = JSON.parse(
|
|
21661
|
+
const metadata = JSON.parse(readFileSync14(packagePath, "utf8"));
|
|
20883
21662
|
if (metadata.name !== "vite" || typeof metadata.bin?.vite !== "string") throw new Error("Installed Vite has no supported CLI entrypoint");
|
|
20884
|
-
const executable = realpathSync3(
|
|
20885
|
-
const executablePart = relative5(realpathSync3(
|
|
20886
|
-
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");
|
|
20887
21666
|
const timeout = options.frontendStartupTimeoutMs ?? 3e4;
|
|
20888
21667
|
if (!Number.isInteger(timeout) || timeout < 100 || timeout > 12e4) throw new Error("Frontend startup timeout must be 100-120000 milliseconds");
|
|
20889
21668
|
const modulePath = realpathSync3(require3.resolve("vite"));
|
|
20890
|
-
const modulePart = relative5(realpathSync3(
|
|
20891
|
-
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");
|
|
20892
21671
|
return { adapter: "vite", directory: directory2, modulePath, timeout, privateFiles };
|
|
20893
21672
|
}
|
|
20894
21673
|
async function stopFrontend(child) {
|
|
@@ -20943,17 +21722,19 @@ async function startLocalFrontend(prepared, port, backendUrl) {
|
|
|
20943
21722
|
}
|
|
20944
21723
|
|
|
20945
21724
|
// src/local-identity.ts
|
|
20946
|
-
import { readFileSync as
|
|
20947
|
-
import { resolve as
|
|
21725
|
+
import { readFileSync as readFileSync15, statSync as statSync2 } from "node:fs";
|
|
21726
|
+
import { resolve as resolve14 } from "node:path";
|
|
20948
21727
|
|
|
20949
21728
|
// ../sdk/dist/local-identity.js
|
|
20950
|
-
import { createHash as
|
|
21729
|
+
import { createHash as createHash12, randomBytes as randomBytes4 } from "node:crypto";
|
|
20951
21730
|
function createLocalIdentityProvider(options) {
|
|
20952
21731
|
const serverOnly = () => {
|
|
20953
21732
|
if (typeof window !== "undefined")
|
|
20954
21733
|
throw new Error("Local development identities are server-only");
|
|
20955
21734
|
};
|
|
20956
21735
|
serverOnly();
|
|
21736
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production")
|
|
21737
|
+
throw new Error("Local Backify identities must not run in production");
|
|
20957
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";
|
|
20958
21739
|
const text3 = (value, maximum) => typeof value === "string" && value.length > 0 && value.length <= maximum && !/[\x00-\x1f\x7f]/.test(value);
|
|
20959
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)
|
|
@@ -20961,9 +21742,11 @@ function createLocalIdentityProvider(options) {
|
|
|
20961
21742
|
const scope = { ...options.scope }, organizationName = options.organizationName;
|
|
20962
21743
|
const users = /* @__PURE__ */ new Map();
|
|
20963
21744
|
for (const user of options.users) {
|
|
20964
|
-
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))
|
|
20965
21746
|
throw new Error("Invalid local identity user");
|
|
20966
|
-
|
|
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));
|
|
20967
21750
|
}
|
|
20968
21751
|
const origins = /* @__PURE__ */ new Set();
|
|
20969
21752
|
for (const value of options.requestOrigins) {
|
|
@@ -20986,7 +21769,7 @@ function createLocalIdentityProvider(options) {
|
|
|
20986
21769
|
return value;
|
|
20987
21770
|
};
|
|
20988
21771
|
const sessions = /* @__PURE__ */ new Map();
|
|
20989
|
-
const digest4 = (token) =>
|
|
21772
|
+
const digest4 = (token) => createHash12("sha256").update(token).digest("hex");
|
|
20990
21773
|
const tokenPattern = /^backify-local-user_[a-zA-Z0-9_-]{43}$/;
|
|
20991
21774
|
const requestAuth = {
|
|
20992
21775
|
forRequest(request) {
|
|
@@ -21003,7 +21786,7 @@ function createLocalIdentityProvider(options) {
|
|
|
21003
21786
|
if (!session || session.issued > time || session.expires <= time)
|
|
21004
21787
|
throw new ApplicationAuthenticationError("application_identity_invalid");
|
|
21005
21788
|
const user = users.get(session.userId);
|
|
21006
|
-
return { ...user, roles: [], claims: { ...scope, localDevelopment: true } };
|
|
21789
|
+
return { ...structuredClone(user), roles: structuredClone(user.roles ?? []), claims: { ...scope, localDevelopment: true } };
|
|
21007
21790
|
};
|
|
21008
21791
|
return { currentUser: async () => identity(), currentOrganization: async () => identity() ? { id: scope.organizationId, name: organizationName } : null };
|
|
21009
21792
|
}
|
|
@@ -21012,7 +21795,7 @@ function createLocalIdentityProvider(options) {
|
|
|
21012
21795
|
requestAuth,
|
|
21013
21796
|
profiles() {
|
|
21014
21797
|
serverOnly();
|
|
21015
|
-
return [...users.values()].map((user) => (
|
|
21798
|
+
return [...users.values()].map((user) => structuredClone(user));
|
|
21016
21799
|
},
|
|
21017
21800
|
issue(userId, lifetimeSeconds = 300) {
|
|
21018
21801
|
const time = now();
|
|
@@ -21023,7 +21806,7 @@ function createLocalIdentityProvider(options) {
|
|
|
21023
21806
|
sessions.delete(hash2);
|
|
21024
21807
|
if (sessions.size >= 128)
|
|
21025
21808
|
throw new ApplicationAuthenticationError("application_identity_unavailable");
|
|
21026
|
-
const accessToken = "backify-local-user_" +
|
|
21809
|
+
const accessToken = "backify-local-user_" + randomBytes4(32).toString("base64url"), expires = time + lifetimeSeconds;
|
|
21027
21810
|
sessions.set(digest4(accessToken), { userId, issued: time, expires });
|
|
21028
21811
|
return { accessToken, tokenType: "Bearer", expiresAt: new Date(expires * 1e3).toISOString() };
|
|
21029
21812
|
},
|
|
@@ -21039,22 +21822,22 @@ function createLocalIdentityProvider(options) {
|
|
|
21039
21822
|
}
|
|
21040
21823
|
|
|
21041
21824
|
// src/local-identity.ts
|
|
21042
|
-
import { randomUUID as
|
|
21825
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
21043
21826
|
function prepareLocalIdentity(root, filename2) {
|
|
21044
21827
|
if (filename2 === void 0) return void 0;
|
|
21045
|
-
const path =
|
|
21828
|
+
const path = resolve14(root, filename2);
|
|
21046
21829
|
assertLocalStatePath(root, path);
|
|
21047
21830
|
if (!statSync2(path).isFile() || statSync2(path).size > 16384) throw new Error("Local identity profiles require a file at most 16 KiB");
|
|
21048
21831
|
let input;
|
|
21049
21832
|
try {
|
|
21050
|
-
input = JSON.parse(
|
|
21833
|
+
input = JSON.parse(readFileSync15(path, "utf8"));
|
|
21051
21834
|
} catch {
|
|
21052
21835
|
throw new Error("Invalid local identity profile JSON");
|
|
21053
21836
|
}
|
|
21054
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");
|
|
21055
21838
|
const value = input;
|
|
21056
21839
|
if (value.version !== 1) throw new Error("Invalid local identity profile version");
|
|
21057
|
-
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"] });
|
|
21058
21841
|
return { organizationName: value.organizationName, users: identity.profiles() };
|
|
21059
21842
|
}
|
|
21060
21843
|
function localIdentityCommand(args2) {
|
|
@@ -21080,17 +21863,17 @@ function localIdentityCommand(args2) {
|
|
|
21080
21863
|
}
|
|
21081
21864
|
|
|
21082
21865
|
// src/local-job-history.ts
|
|
21083
|
-
import { lstatSync as
|
|
21084
|
-
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";
|
|
21085
21868
|
function prepareLocalJobHistory(root, filename2, artifacts, manifest, current) {
|
|
21086
21869
|
if (filename2 === void 0) return [];
|
|
21087
21870
|
const read = (filename3, maximum) => {
|
|
21088
|
-
if (typeof filename3 !== "string" || !filename3 ||
|
|
21089
|
-
const path =
|
|
21871
|
+
if (typeof filename3 !== "string" || !filename3 || isAbsolute11(filename3)) throw new Error();
|
|
21872
|
+
const path = resolve15(root, filename3);
|
|
21090
21873
|
assertLocalStatePath(root, path);
|
|
21091
|
-
const metadata =
|
|
21874
|
+
const metadata = lstatSync13(path);
|
|
21092
21875
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > maximum) throw new Error();
|
|
21093
|
-
const bytes =
|
|
21876
|
+
const bytes = readFileSync16(path);
|
|
21094
21877
|
if (bytes.length > maximum) throw new Error();
|
|
21095
21878
|
return new TextDecoder("utf8", { fatal: true }).decode(bytes);
|
|
21096
21879
|
};
|
|
@@ -21100,7 +21883,7 @@ function prepareLocalJobHistory(root, filename2, artifacts, manifest, current) {
|
|
|
21100
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();
|
|
21101
21884
|
const versions = /* @__PURE__ */ new Set([current.artifact.artifactVersion]);
|
|
21102
21885
|
for (const item of input.workers) {
|
|
21103
|
-
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();
|
|
21104
21887
|
versions.add(item.artifactVersion);
|
|
21105
21888
|
}
|
|
21106
21889
|
return input.workers.map((item) => {
|
|
@@ -21136,30 +21919,30 @@ function prepareLocalJobHistory(root, filename2, artifacts, manifest, current) {
|
|
|
21136
21919
|
}
|
|
21137
21920
|
|
|
21138
21921
|
// src/local-secrets.ts
|
|
21139
|
-
import { createCipheriv, createDecipheriv, randomBytes as
|
|
21140
|
-
import { closeSync as closeSync3, existsSync as
|
|
21141
|
-
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";
|
|
21142
21925
|
var maximumBytes2 = 1048576;
|
|
21143
21926
|
var secretName = /^[A-Z][A-Z0-9_]{0,127}$/;
|
|
21144
21927
|
var failure = () => new Error("Local secret vault unavailable or invalid; review the vault and external key privately");
|
|
21145
21928
|
function vaultPath(root) {
|
|
21146
|
-
return
|
|
21929
|
+
return join11(realpathSync4(root), ".backify", "dev", "secrets.enc.json");
|
|
21147
21930
|
}
|
|
21148
21931
|
function inside(root, filename2) {
|
|
21149
21932
|
const part = relative6(root, filename2);
|
|
21150
|
-
return part !== ".." && !part.startsWith("../") && !part.startsWith("..\\") && !
|
|
21933
|
+
return part !== ".." && !part.startsWith("../") && !part.startsWith("..\\") && !isAbsolute12(part);
|
|
21151
21934
|
}
|
|
21152
21935
|
function privateFile(filename2, limit) {
|
|
21153
|
-
for (let current =
|
|
21154
|
-
if (
|
|
21155
|
-
if (
|
|
21936
|
+
for (let current = resolve16(filename2); ; current = dirname11(current)) {
|
|
21937
|
+
if (lstatSync14(current).isSymbolicLink()) throw failure();
|
|
21938
|
+
if (dirname11(current) === current) break;
|
|
21156
21939
|
}
|
|
21157
|
-
const stat =
|
|
21940
|
+
const stat = lstatSync14(filename2);
|
|
21158
21941
|
if (!stat.isFile() || stat.size > limit || stat.nlink !== 1 || process.platform !== "win32" && (stat.mode & 63) !== 0) throw failure();
|
|
21159
|
-
return
|
|
21942
|
+
return readFileSync17(filename2);
|
|
21160
21943
|
}
|
|
21161
21944
|
function externalKey(root, filename2) {
|
|
21162
|
-
const keyFile =
|
|
21945
|
+
const keyFile = resolve16(filename2);
|
|
21163
21946
|
if (inside(realpathSync4(root), keyFile) || inside(realpathSync4(root), realpathSync4(keyFile))) throw failure();
|
|
21164
21947
|
const encoded = privateFile(keyFile, 64).toString("utf8");
|
|
21165
21948
|
if (!/^[a-f0-9]{64}$/.test(encoded)) throw failure();
|
|
@@ -21173,7 +21956,7 @@ function context(root, application) {
|
|
|
21173
21956
|
}
|
|
21174
21957
|
function encrypt(root, application, key, secrets) {
|
|
21175
21958
|
validateSecrets(secrets);
|
|
21176
|
-
const nonce =
|
|
21959
|
+
const nonce = randomBytes5(12), cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
21177
21960
|
cipher.setAAD(context(root, application));
|
|
21178
21961
|
const plaintext = Buffer.from(JSON.stringify(secrets));
|
|
21179
21962
|
try {
|
|
@@ -21188,22 +21971,22 @@ function encrypt(root, application, key, secrets) {
|
|
|
21188
21971
|
function writePrivate2(filename2, value) {
|
|
21189
21972
|
const descriptor2 = openSync3(filename2, "wx", 384);
|
|
21190
21973
|
try {
|
|
21191
|
-
|
|
21974
|
+
writeFileSync9(descriptor2, value);
|
|
21192
21975
|
fsyncSync3(descriptor2);
|
|
21193
21976
|
} finally {
|
|
21194
21977
|
closeSync3(descriptor2);
|
|
21195
21978
|
}
|
|
21196
21979
|
}
|
|
21197
21980
|
function ensureIgnored(root) {
|
|
21198
|
-
const filename2 =
|
|
21981
|
+
const filename2 = join11(root, ".gitignore");
|
|
21199
21982
|
assertLocalStatePath(root, filename2);
|
|
21200
|
-
const current =
|
|
21201
|
-
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");
|
|
21202
21985
|
}
|
|
21203
21986
|
function readLocalSecrets(root, application, keyFile) {
|
|
21204
21987
|
const filename2 = vaultPath(root);
|
|
21205
21988
|
assertLocalStatePath(root, filename2);
|
|
21206
|
-
if (!
|
|
21989
|
+
if (!existsSync10(filename2) && keyFile === void 0) return {};
|
|
21207
21990
|
if (!keyFile) throw new Error("Encrypted local secrets require --local-secrets-key <external-key-file>");
|
|
21208
21991
|
let key, plaintext;
|
|
21209
21992
|
try {
|
|
@@ -21231,27 +22014,27 @@ function readLocalSecrets(root, application, keyFile) {
|
|
|
21231
22014
|
}
|
|
21232
22015
|
}
|
|
21233
22016
|
function initializeLocalSecrets(root, application, keyFile) {
|
|
21234
|
-
const filename2 = vaultPath(root), external =
|
|
22017
|
+
const filename2 = vaultPath(root), external = resolve16(keyFile);
|
|
21235
22018
|
assertLocalStatePath(root, filename2);
|
|
21236
|
-
if (
|
|
21237
|
-
if (inside(realpathSync4(root), external) || inside(realpathSync4(root), realpathSync4(
|
|
21238
|
-
for (let current =
|
|
21239
|
-
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);
|
|
21240
22023
|
if (stat.isSymbolicLink()) throw failure();
|
|
21241
|
-
if (current ===
|
|
21242
|
-
if (
|
|
22024
|
+
if (current === dirname11(external) && process.platform !== "win32" && (stat.mode & 63) !== 0) throw failure();
|
|
22025
|
+
if (dirname11(current) === current) break;
|
|
21243
22026
|
}
|
|
21244
22027
|
ensureIgnored(root);
|
|
21245
|
-
const
|
|
21246
|
-
const key =
|
|
22028
|
+
const release2 = acquireLocalLease(root, ".backify/dev/secrets.lock");
|
|
22029
|
+
const key = randomBytes5(32);
|
|
21247
22030
|
try {
|
|
21248
|
-
if (
|
|
21249
|
-
|
|
22031
|
+
if (existsSync10(filename2)) throw failure();
|
|
22032
|
+
mkdirSync5(dirname11(filename2), { recursive: true, mode: 448 });
|
|
21250
22033
|
writePrivate2(external, key.toString("hex"));
|
|
21251
22034
|
writePrivate2(filename2, encrypt(root, application, key, {}));
|
|
21252
22035
|
} finally {
|
|
21253
22036
|
key.fill(0);
|
|
21254
|
-
|
|
22037
|
+
release2();
|
|
21255
22038
|
}
|
|
21256
22039
|
return { schemaVersion: 1, status: "initialized", keyFile: external, names: [] };
|
|
21257
22040
|
}
|
|
@@ -21259,19 +22042,19 @@ function setLocalSecret(root, application, keyFile, name2, value) {
|
|
|
21259
22042
|
if (!secretName.test(name2) || Buffer.byteLength(value) < 1 || Buffer.byteLength(value) > 65536) throw new Error("Local secrets require an uppercase logical name and 1-65536 UTF-8 bytes from stdin");
|
|
21260
22043
|
const filename2 = vaultPath(root);
|
|
21261
22044
|
assertLocalStatePath(root, filename2);
|
|
21262
|
-
const
|
|
22045
|
+
const release2 = acquireLocalLease(root, ".backify/dev/secrets.lock");
|
|
21263
22046
|
let key;
|
|
21264
22047
|
try {
|
|
21265
22048
|
const secrets = readLocalSecrets(root, application, keyFile);
|
|
21266
22049
|
secrets[name2] = value;
|
|
21267
22050
|
key = externalKey(root, keyFile);
|
|
21268
|
-
const temporary = filename2 + "." +
|
|
22051
|
+
const temporary = filename2 + "." + randomUUID8() + ".tmp";
|
|
21269
22052
|
assertLocalStatePath(root, temporary);
|
|
21270
22053
|
writePrivate2(temporary, encrypt(root, application, key, secrets));
|
|
21271
22054
|
renameLocalFile(temporary, filename2);
|
|
21272
22055
|
} finally {
|
|
21273
22056
|
key?.fill(0);
|
|
21274
|
-
|
|
22057
|
+
release2();
|
|
21275
22058
|
}
|
|
21276
22059
|
return { schemaVersion: 1, status: "stored", name: name2 };
|
|
21277
22060
|
}
|
|
@@ -21302,27 +22085,27 @@ async function localSecretsCommand(args2, root, application, input = process.std
|
|
|
21302
22085
|
|
|
21303
22086
|
// src/local-development.ts
|
|
21304
22087
|
function localManifest(root = process.cwd()) {
|
|
21305
|
-
const filename2 =
|
|
21306
|
-
if (!
|
|
21307
|
-
const manifest = parseApplicationManifest(
|
|
21308
|
-
const entry =
|
|
21309
|
-
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");
|
|
21310
22093
|
const discovered = analyzeSources(repositorySources(root));
|
|
21311
|
-
const diagnostics = manifestDiagnostics(manifest, Object.keys(discovered.capabilities));
|
|
22094
|
+
const diagnostics = manifestDiagnostics(manifest, Object.keys(discovered.capabilities), discovered.capabilities.auth?.configuration);
|
|
21312
22095
|
if (diagnostics.length) throw new Error(diagnostics.map((diagnostic) => diagnostic.message).join("; "));
|
|
21313
22096
|
return manifest;
|
|
21314
22097
|
}
|
|
21315
22098
|
function inside2(root, path) {
|
|
21316
22099
|
const part = relative7(realpathSync5(root), path);
|
|
21317
|
-
return part !== ".." && !part.startsWith(".." + (process.platform === "win32" ? "\\" : "/")) && !
|
|
22100
|
+
return part !== ".." && !part.startsWith(".." + (process.platform === "win32" ? "\\" : "/")) && !isAbsolute13(part);
|
|
21318
22101
|
}
|
|
21319
22102
|
function sessionPath(root) {
|
|
21320
|
-
return
|
|
22103
|
+
return join12(root, ".backify", "dev", "session.json");
|
|
21321
22104
|
}
|
|
21322
22105
|
async function localRequest(path, input, root = process.cwd()) {
|
|
21323
22106
|
if (!/^\/v1\/[a-z/-]+$/.test(path)) throw new Error("Invalid local command path");
|
|
21324
|
-
if (!
|
|
21325
|
-
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"));
|
|
21326
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");
|
|
21327
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) });
|
|
21328
22111
|
if (!response.ok) {
|
|
@@ -21355,7 +22138,7 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21355
22138
|
if (options.jobTrustSyncFile !== void 0 && options.jobAuthFile === void 0) throw new Error("Native worker trust renewal requires explicit reviewed auth mounts");
|
|
21356
22139
|
if (options.jobHistoryFile !== void 0 && options.jobWorkerDirectory === void 0) throw new Error("Historical workers require paired reviewed job artifacts and current worker");
|
|
21357
22140
|
if (options.jobAuthFile !== void 0 && options.identityUsers !== void 0) throw new Error("Reviewed worker issuer trust cannot be mixed with local identity fixtures");
|
|
21358
|
-
const root =
|
|
22141
|
+
const root = resolve17(options.root ?? process.cwd());
|
|
21359
22142
|
const manifest = localManifest(root);
|
|
21360
22143
|
const validateSecretIntent = (values, intent = manifest) => {
|
|
21361
22144
|
if (Object.keys(values).length && (!intent.capabilities.secrets || intent.capabilities.secrets.enabled === false)) throw new Error("Encrypted local secrets require enabled capabilities.secrets");
|
|
@@ -21372,18 +22155,18 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21372
22155
|
createLocalJobTrustRenewal(root, authMounts);
|
|
21373
22156
|
const identityProfiles = prepareLocalIdentity(root, options.identityUsers);
|
|
21374
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");
|
|
21375
|
-
const preparedFrontend = prepareLocalFrontend(root, options, options.localSecretsKey ? [realpathSync5(
|
|
21376
|
-
const ignorePath =
|
|
21377
|
-
if (
|
|
21378
|
-
const ignore =
|
|
21379
|
-
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");
|
|
21380
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");
|
|
21381
|
-
const directory2 =
|
|
22164
|
+
const directory2 = join12(root, ".backify", "dev");
|
|
21382
22165
|
assertLocalStatePath(root, directory2);
|
|
21383
|
-
for (const filename2 of ["session.json", "session.json.tmp", "worker.mjs", "worker.mjs.map", "state.json", "state.json.tmp"]) assertLocalStatePath(root,
|
|
21384
|
-
|
|
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 });
|
|
21385
22168
|
if (!inside2(root, realpathSync5(directory2))) throw new Error("Local state directory must remain inside this repository");
|
|
21386
|
-
if (
|
|
22169
|
+
if (existsSync11(sessionPath(root))) {
|
|
21387
22170
|
try {
|
|
21388
22171
|
await localRequest("/v1/status", void 0, root);
|
|
21389
22172
|
throw new Error("Backify dev is already running for this repository");
|
|
@@ -21413,15 +22196,15 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21413
22196
|
}
|
|
21414
22197
|
throw error;
|
|
21415
22198
|
}
|
|
21416
|
-
const token =
|
|
21417
|
-
const identityScope = services?.identityScope ?? { organizationId:
|
|
22199
|
+
const token = randomBytes6(32).toString("hex");
|
|
22200
|
+
const identityScope = services?.identityScope ?? { organizationId: randomUUID9(), applicationId: randomUUID9(), environmentId: randomUUID9() };
|
|
21418
22201
|
const workerEnvironment = (worker) => {
|
|
21419
22202
|
if (!worker.authTrust) return worker;
|
|
21420
22203
|
const { renewal, ...authTrust } = worker.authTrust;
|
|
21421
22204
|
return { ...worker, authTrust };
|
|
21422
22205
|
};
|
|
21423
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" });
|
|
21424
|
-
const outfile =
|
|
22207
|
+
const outfile = join12(directory2, "worker.mjs");
|
|
21425
22208
|
let child;
|
|
21426
22209
|
const trustRenewal = createLocalJobTrustRenewal(root, authMounts, { onFailure: () => console.error("Native worker trust renewal unavailable; existing validity and pin guards remain enforced."), onUpdate: () => {
|
|
21427
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) => {
|
|
@@ -21459,8 +22242,8 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21459
22242
|
}
|
|
21460
22243
|
await stopChild(child);
|
|
21461
22244
|
if (stopping) return;
|
|
21462
|
-
const version =
|
|
21463
|
-
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" } });
|
|
21464
22247
|
child = worker;
|
|
21465
22248
|
worker.send({ type: "local-secrets", secrets }, (error) => {
|
|
21466
22249
|
if (error) {
|
|
@@ -21482,7 +22265,7 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21482
22265
|
workerReady = true;
|
|
21483
22266
|
const session = { version: 1, port: message.port, token, pid: worker.pid };
|
|
21484
22267
|
const temp = sessionPath(root) + ".tmp";
|
|
21485
|
-
|
|
22268
|
+
writeFileSync10(temp, JSON.stringify(session), { mode: 384 });
|
|
21486
22269
|
renameLocalFile(temp, sessionPath(root));
|
|
21487
22270
|
const url = "http://127.0.0.1:" + message.port;
|
|
21488
22271
|
options.onReady?.(url, { frontendUrl: frontend?.url, backendUrl });
|
|
@@ -21494,17 +22277,17 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21494
22277
|
});
|
|
21495
22278
|
};
|
|
21496
22279
|
try {
|
|
21497
|
-
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) {
|
|
21498
22281
|
build3.onLoad({ filter: /./, namespace: "file" }, (input) => {
|
|
21499
|
-
if (input.path !==
|
|
22282
|
+
if (input.path !== resolve17(root, manifest.runtime.entrypoint)) return;
|
|
21500
22283
|
try {
|
|
21501
22284
|
const updated = localManifest(root);
|
|
21502
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");
|
|
21503
22286
|
if (updated.runtime.entrypoint !== manifest.runtime.entrypoint) throw new Error("Runtime entrypoint changed; restart backify dev");
|
|
21504
22287
|
} catch (error) {
|
|
21505
|
-
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")] };
|
|
21506
22289
|
}
|
|
21507
|
-
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")] };
|
|
21508
22291
|
});
|
|
21509
22292
|
build3.onEnd((result) => {
|
|
21510
22293
|
if (result.errors.length) rejectStartup(new Error("Local application build failed"));
|
|
@@ -21560,8 +22343,8 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21560
22343
|
}
|
|
21561
22344
|
}
|
|
21562
22345
|
}
|
|
21563
|
-
if (
|
|
21564
|
-
const session = JSON.parse(
|
|
22346
|
+
if (existsSync11(sessionPath(root))) {
|
|
22347
|
+
const session = JSON.parse(readFileSync18(sessionPath(root), "utf8"));
|
|
21565
22348
|
if (session.token === token) unlinkSync4(sessionPath(root));
|
|
21566
22349
|
}
|
|
21567
22350
|
};
|
|
@@ -21575,11 +22358,11 @@ async function startLocalDevelopment(options = {}) {
|
|
|
21575
22358
|
}
|
|
21576
22359
|
async function runLocalTests(root = process.cwd()) {
|
|
21577
22360
|
const manifest = localManifest(root);
|
|
21578
|
-
const project = JSON.parse(
|
|
22361
|
+
const project = JSON.parse(readFileSync18(join12(root, "package.json"), "utf8"));
|
|
21579
22362
|
if (!project.scripts?.test) throw new Error("Add a test script to package.json so AI agents can run backify test");
|
|
21580
22363
|
const result = await new Promise((resolveResult, reject) => {
|
|
21581
|
-
const npmPath = process.platform === "win32" ? process.env.npm_execpath ??
|
|
21582
|
-
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)) {
|
|
21583
22366
|
reject(new Error("Cannot locate npm-cli.js; run backify test through npx or npm"));
|
|
21584
22367
|
return;
|
|
21585
22368
|
}
|
|
@@ -21599,15 +22382,15 @@ async function runLocalTests(root = process.cwd()) {
|
|
|
21599
22382
|
// src/workload.ts
|
|
21600
22383
|
function inside3(root, filename2) {
|
|
21601
22384
|
const part = relative8(root, filename2);
|
|
21602
|
-
return part !== ".." && !part.startsWith("../") && !part.startsWith("..\\") && !
|
|
22385
|
+
return part !== ".." && !part.startsWith("../") && !part.startsWith("..\\") && !isAbsolute14(part);
|
|
21603
22386
|
}
|
|
21604
22387
|
function digest3(contents) {
|
|
21605
|
-
return
|
|
22388
|
+
return createHash14("sha256").update(contents).digest("hex");
|
|
21606
22389
|
}
|
|
21607
22390
|
function packageDirectory(filename2) {
|
|
21608
|
-
for (let current =
|
|
21609
|
-
if (
|
|
21610
|
-
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");
|
|
21611
22394
|
}
|
|
21612
22395
|
}
|
|
21613
22396
|
function workloadSource(entry, kind, capabilities) {
|
|
@@ -21688,11 +22471,11 @@ main().catch(() => finish({ schemaVersion: 1, status: "failed", error: "workload
|
|
|
21688
22471
|
`.replace("__KIND__", JSON.stringify(kind)).replace("__CAPABILITIES__", JSON.stringify(capabilities)).replace("__AUTH_ENABLED__", String(capabilities.includes("auth"))).replace("__ENTRY__", JSON.stringify(entry));
|
|
21689
22472
|
}
|
|
21690
22473
|
async function prepareWorkload(options) {
|
|
21691
|
-
const root = realpathSync6(
|
|
22474
|
+
const root = realpathSync6(resolve18(options.root ?? process.cwd()));
|
|
21692
22475
|
const manifest = localManifest(root);
|
|
21693
22476
|
if (options.kind !== "http" && options.kind !== "worker") throw new Error("Workload kind must be http or worker");
|
|
21694
22477
|
if (options.kind === "worker" && !options.entry) throw new Error("Workers require an explicit declaration-only entrypoint");
|
|
21695
|
-
const entry = realpathSync6(
|
|
22478
|
+
const entry = realpathSync6(resolve18(root, options.entry ?? manifest.runtime.entrypoint));
|
|
21696
22479
|
if (!inside3(root, entry)) throw new Error("Workload entrypoint must stay inside this repository");
|
|
21697
22480
|
const enabled = Object.entries(manifest.capabilities).filter(([, settings]) => settings?.enabled !== false).map(([name2]) => name2).sort();
|
|
21698
22481
|
if (options.kind === "worker" && !enabled.includes("jobs")) throw new Error("Workers require declared jobs capability");
|
|
@@ -21718,14 +22501,14 @@ async function prepareWorkload(options) {
|
|
|
21718
22501
|
context3.onResolve({ filter: /./ }, async (input) => {
|
|
21719
22502
|
if (input.pluginData?.backifyResolved) return;
|
|
21720
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" }] };
|
|
21721
|
-
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 } });
|
|
21722
22505
|
if (!resolved.external && !resolved.errors.length && resolved.path) {
|
|
21723
22506
|
const directory2 = realpathSync6(packageDirectory(resolved.path));
|
|
21724
22507
|
const target = relative8(directory2, realpathSync6(resolved.path)).replaceAll("\\", "/");
|
|
21725
|
-
if (JSON.parse(
|
|
22508
|
+
if (JSON.parse(readFileSync19(join13(directory2, "package.json"), "utf8")).name === "@backifydev/sdk") {
|
|
21726
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" }] };
|
|
21727
22510
|
}
|
|
21728
|
-
if (!
|
|
22511
|
+
if (!isAbsolute14(input.path) && !input.path.startsWith(".")) trustedPackages.add(directory2);
|
|
21729
22512
|
}
|
|
21730
22513
|
return resolved;
|
|
21731
22514
|
});
|
|
@@ -21784,34 +22567,34 @@ async function packageWorkload(options) {
|
|
|
21784
22567
|
if (plan.blockers.includes("bundle-warning-review-required")) throw new Error("Workload bundle warnings require review before packaging");
|
|
21785
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");
|
|
21786
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" };
|
|
21787
|
-
const output =
|
|
22570
|
+
const output = resolve18(root, options.output ?? ".backify/artifacts/" + options.kind + "-" + plan.artifactVersion.slice(0, 16));
|
|
21788
22571
|
assertLocalStatePath(root, output);
|
|
21789
|
-
if (
|
|
21790
|
-
if (JSON.stringify(
|
|
21791
|
-
assertLocalStatePath(root,
|
|
21792
|
-
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));
|
|
21793
22576
|
})) throw new Error("Refusing to overwrite an existing workload artifact directory");
|
|
21794
22577
|
} else {
|
|
21795
|
-
|
|
21796
|
-
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 });
|
|
21797
22580
|
}
|
|
21798
22581
|
return { ...plan, outputDirectory: relative8(root, output).replaceAll("\\", "/") };
|
|
21799
22582
|
}
|
|
21800
22583
|
|
|
21801
22584
|
// src/workload-provenance.ts
|
|
21802
22585
|
import { execFileSync } from "node:child_process";
|
|
21803
|
-
import { mkdirSync as
|
|
22586
|
+
import { mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync2, readdirSync as readdirSync7, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "node:fs";
|
|
21804
22587
|
import { tmpdir } from "node:os";
|
|
21805
|
-
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";
|
|
21806
22589
|
|
|
21807
22590
|
// src/workload-provenance-oci.ts
|
|
21808
|
-
import { createHash as
|
|
21809
|
-
import { createReadStream, lstatSync as
|
|
21810
|
-
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";
|
|
21811
22594
|
import { Transform, Writable } from "node:stream";
|
|
21812
22595
|
import { pipeline } from "node:stream/promises";
|
|
21813
22596
|
import { createGunzip } from "node:zlib";
|
|
21814
|
-
var provenanceHash = (bytes) => "sha256:" +
|
|
22597
|
+
var provenanceHash = (bytes) => "sha256:" + createHash15("sha256").update(bytes).digest("hex");
|
|
21815
22598
|
var maximumLayer = 256 * 1024 * 1024;
|
|
21816
22599
|
var maximumImage = 1024 * 1024 * 1024;
|
|
21817
22600
|
var manifestTypes = ["application/vnd.oci.image.manifest.v1+json", "application/vnd.docker.distribution.manifest.v2+json"];
|
|
@@ -21822,14 +22605,14 @@ function provenanceRef(value) {
|
|
|
21822
22605
|
return value.slice(value.lastIndexOf("@") + 1);
|
|
21823
22606
|
}
|
|
21824
22607
|
function provenanceFile(filename2, maximum) {
|
|
21825
|
-
const absolute =
|
|
21826
|
-
for (let ancestor = absolute; ; ancestor =
|
|
21827
|
-
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");
|
|
21828
22611
|
if (ancestor === parse3(absolute).root) break;
|
|
21829
22612
|
}
|
|
21830
|
-
const stat =
|
|
22613
|
+
const stat = lstatSync16(absolute);
|
|
21831
22614
|
if (!stat.isFile() || stat.nlink !== 1 || stat.size < 1 || stat.size > maximum) throw new Error("Invalid provenance file");
|
|
21832
|
-
const bytes =
|
|
22615
|
+
const bytes = readFileSync20(absolute);
|
|
21833
22616
|
if (bytes.length !== stat.size) throw new Error("Provenance file changed during read");
|
|
21834
22617
|
return bytes;
|
|
21835
22618
|
}
|
|
@@ -21838,7 +22621,7 @@ function descriptor(value) {
|
|
|
21838
22621
|
return value;
|
|
21839
22622
|
}
|
|
21840
22623
|
function blobPath(root, value) {
|
|
21841
|
-
return
|
|
22624
|
+
return join14(root, "blobs", "sha256", descriptor(value).digest.slice(7));
|
|
21842
22625
|
}
|
|
21843
22626
|
function jsonBlob(root, value) {
|
|
21844
22627
|
const bytes = provenanceFile(blobPath(root, value), 1024 * 1024);
|
|
@@ -21847,8 +22630,8 @@ function jsonBlob(root, value) {
|
|
|
21847
22630
|
}
|
|
21848
22631
|
async function inspectProvenanceImage(root, architecture, requestedDigest) {
|
|
21849
22632
|
if (!["amd64", "arm64"].includes(architecture) || requestedDigest && !digestPattern.test(requestedDigest)) throw new Error("Invalid OCI platform or digest");
|
|
21850
|
-
if (JSON.parse(provenanceFile(
|
|
21851
|
-
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);
|
|
21852
22635
|
let current = JSON.parse(indexBytes.toString());
|
|
21853
22636
|
let selected;
|
|
21854
22637
|
let manifestDigest = provenanceHash(indexBytes);
|
|
@@ -21879,7 +22662,7 @@ async function inspectProvenanceImage(root, architecture, requestedDigest) {
|
|
|
21879
22662
|
if (!digestPattern.test(config.rootfs.diff_ids[index])) throw new Error("Invalid OCI diff ID");
|
|
21880
22663
|
const filename2 = blobPath(root, layer);
|
|
21881
22664
|
provenanceFileMetadata(filename2, layer.size);
|
|
21882
|
-
const storedHash =
|
|
22665
|
+
const storedHash = createHash15("sha256"), expandedHash = createHash15("sha256");
|
|
21883
22666
|
let stored = 0, expanded = 0;
|
|
21884
22667
|
const counter = new Transform({ transform(chunk, _encoding, callback) {
|
|
21885
22668
|
stored += chunk.length;
|
|
@@ -21905,12 +22688,12 @@ async function inspectProvenanceImage(root, architecture, requestedDigest) {
|
|
|
21905
22688
|
return { manifestDigest, configDigest: configuration.digest, architecture, layers };
|
|
21906
22689
|
}
|
|
21907
22690
|
function provenanceFileMetadata(filename2, expectedSize) {
|
|
21908
|
-
const absolute =
|
|
21909
|
-
for (let ancestor = absolute; ; ancestor =
|
|
21910
|
-
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");
|
|
21911
22694
|
if (ancestor === parse3(absolute).root) break;
|
|
21912
22695
|
}
|
|
21913
|
-
const stat =
|
|
22696
|
+
const stat = lstatSync16(absolute);
|
|
21914
22697
|
if (!stat.isFile() || stat.nlink !== 1 || stat.size !== expectedSize) throw new Error("Invalid OCI layer file");
|
|
21915
22698
|
}
|
|
21916
22699
|
function verifyBaseLineage(base, image2) {
|
|
@@ -21933,7 +22716,7 @@ function canonical(value) {
|
|
|
21933
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(",") + "}";
|
|
21934
22717
|
return JSON.stringify(value);
|
|
21935
22718
|
}
|
|
21936
|
-
function
|
|
22719
|
+
function git2(root, args2) {
|
|
21937
22720
|
try {
|
|
21938
22721
|
return execFileSync("git", ["-C", root, ...args2], { encoding: "utf8", windowsHide: true, timeout: 3e4, maxBuffer: 16 * 1024 * 1024 }).trim();
|
|
21939
22722
|
} catch {
|
|
@@ -21968,28 +22751,28 @@ function dependencyMaterials(lock) {
|
|
|
21968
22751
|
return { digest: provenanceHash(canonical(materials.sort((left, right) => left.path.localeCompare(right.path, "en")))), count: materials.length };
|
|
21969
22752
|
}
|
|
21970
22753
|
async function captureWorkloadProvenance(options) {
|
|
21971
|
-
const root =
|
|
22754
|
+
const root = resolve20(options.root ?? process.cwd());
|
|
21972
22755
|
if (!repositoryPattern.test(options.repository) || !["first", "second"].includes(options.replica)) throw new Error("Invalid provenance repository or replica");
|
|
21973
|
-
const applicationPath = relative9(root,
|
|
21974
|
-
if (!applicationPath ||
|
|
21975
|
-
if (
|
|
21976
|
-
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}"]);
|
|
21977
22760
|
if (!sha.test(commit) || !sha.test(tree)) throw new Error("Full Git commit and tree required");
|
|
21978
|
-
const origin =
|
|
22761
|
+
const origin = git2(root, ["remote", "get-url", "origin"]);
|
|
21979
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");
|
|
21980
|
-
const lockBytes = provenanceFile(
|
|
22763
|
+
const lockBytes = provenanceFile(join15(root, "package-lock.json"), 16 * 1024 * 1024);
|
|
21981
22764
|
const dependencies = dependencyMaterials(JSON.parse(lockBytes.toString("utf8")));
|
|
21982
|
-
const context3 =
|
|
21983
|
-
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");
|
|
21984
22767
|
const contextDigests = {};
|
|
21985
|
-
for (const name2 of contextNames) contextDigests[name2] = provenanceHash(provenanceFile(
|
|
21986
|
-
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"));
|
|
21987
22770
|
const baseDigest = provenanceRef(options.baseImage);
|
|
21988
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");
|
|
21989
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");
|
|
21990
|
-
if (!provenanceFile(
|
|
21991
|
-
const base = await inspectProvenanceImage(
|
|
21992
|
-
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);
|
|
21993
22776
|
verifyBaseLineage(base, image2);
|
|
21994
22777
|
let npm;
|
|
21995
22778
|
try {
|
|
@@ -22011,7 +22794,7 @@ function validateReport(report) {
|
|
|
22011
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");
|
|
22012
22795
|
for (const path of [report.build.applicationPath, report.build.entry]) {
|
|
22013
22796
|
text2(path);
|
|
22014
|
-
if (
|
|
22797
|
+
if (isAbsolute15(path) || path.includes(":") || path.includes("\\") || path.split("/").includes("..")) throw new Error("Invalid build source path");
|
|
22015
22798
|
}
|
|
22016
22799
|
provenanceRef(report.build.baseImage);
|
|
22017
22800
|
for (const image2 of [report.base, report.image]) {
|
|
@@ -22062,17 +22845,17 @@ function verifyWorkloadProvenance(options, now = /* @__PURE__ */ new Date()) {
|
|
|
22062
22845
|
if (provenanceHash(baseSubject) !== provenanceRef(policy.baseImage)) throw new Error("Base attestation subject does not match the approved image digest");
|
|
22063
22846
|
evidence.push({ bytes: baseSubject, bundle: provenanceFile(options.baseBundle, 4 * 1024 * 1024), identity: { ...policy, ...supplier } });
|
|
22064
22847
|
}
|
|
22065
|
-
const scratchRoot =
|
|
22066
|
-
const scratch =
|
|
22067
|
-
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");
|
|
22068
22851
|
try {
|
|
22069
|
-
|
|
22070
|
-
const trustedRoot =
|
|
22071
|
-
|
|
22852
|
+
mkdirSync8(join15(scratch, "evidence"), { mode: 448 });
|
|
22853
|
+
const trustedRoot = join15(scratch, "evidence", "trusted-root.jsonl");
|
|
22854
|
+
writeFileSync12(trustedRoot, rootBytes, { flag: "wx", mode: 384 });
|
|
22072
22855
|
for (const [index, { bytes, bundle: bundleBytes, identity }] of evidence.entries()) {
|
|
22073
|
-
const report =
|
|
22074
|
-
|
|
22075
|
-
|
|
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 });
|
|
22076
22859
|
try {
|
|
22077
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"] });
|
|
22078
22861
|
if (!Array.isArray(JSON.parse(result)) || !JSON.parse(result).length) throw new Error();
|
|
@@ -22081,7 +22864,7 @@ function verifyWorkloadProvenance(options, now = /* @__PURE__ */ new Date()) {
|
|
|
22081
22864
|
}
|
|
22082
22865
|
}
|
|
22083
22866
|
} finally {
|
|
22084
|
-
|
|
22867
|
+
rmSync2(scratch, { recursive: true, force: true });
|
|
22085
22868
|
}
|
|
22086
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"] };
|
|
22087
22870
|
}
|
|
@@ -22103,13 +22886,13 @@ async function workloadProvenanceCommand(args2) {
|
|
|
22103
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") });
|
|
22104
22887
|
}
|
|
22105
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" });
|
|
22106
|
-
|
|
22889
|
+
writeFileSync12(resolve20(required("--output")), JSON.stringify(result, null, 2) + "\n", { flag: "wx", mode: 384 });
|
|
22107
22890
|
return { schemaVersion: 1, status: "captured-unsigned", deploymentReady: false, reportDigest: provenanceHash(JSON.stringify(result, null, 2) + "\n") };
|
|
22108
22891
|
}
|
|
22109
22892
|
|
|
22110
22893
|
// src/workload-artifact-plan.ts
|
|
22111
|
-
import { existsSync as
|
|
22112
|
-
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";
|
|
22113
22896
|
function uuid4(value) {
|
|
22114
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";
|
|
22115
22898
|
}
|
|
@@ -22147,14 +22930,14 @@ function parseWorkerArtifactPlan(args2) {
|
|
|
22147
22930
|
}
|
|
22148
22931
|
async function planWorkerArtifactRegistration(options) {
|
|
22149
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");
|
|
22150
|
-
const root = realpathSync7(
|
|
22933
|
+
const root = realpathSync7(resolve21(options.root ?? process.cwd()));
|
|
22151
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");
|
|
22152
|
-
const filename2 =
|
|
22935
|
+
const filename2 = resolve21(root, options.jobsFile);
|
|
22153
22936
|
assertLocalStatePath(root, filename2);
|
|
22154
|
-
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");
|
|
22155
22938
|
let jobs;
|
|
22156
22939
|
try {
|
|
22157
|
-
jobs = jobMetadata(JSON.parse(
|
|
22940
|
+
jobs = jobMetadata(JSON.parse(readFileSync21(filename2, "utf8")));
|
|
22158
22941
|
} catch {
|
|
22159
22942
|
throw new Error("Invalid bounded worker job metadata");
|
|
22160
22943
|
}
|
|
@@ -22175,8 +22958,8 @@ async function planWorkerArtifactRegistration(options) {
|
|
|
22175
22958
|
}
|
|
22176
22959
|
|
|
22177
22960
|
// src/workload-trust-state.ts
|
|
22178
|
-
import { lstatSync as
|
|
22179
|
-
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";
|
|
22180
22963
|
function parseWorkloadTrustState(args2) {
|
|
22181
22964
|
const action = args2[0];
|
|
22182
22965
|
if (action !== "plan" && action !== "initialize") throw new Error("Use backify workload trust-state plan|initialize");
|
|
@@ -22198,12 +22981,12 @@ function parseWorkloadTrustState(args2) {
|
|
|
22198
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") };
|
|
22199
22982
|
}
|
|
22200
22983
|
function workloadTrustState(options) {
|
|
22201
|
-
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");
|
|
22202
22985
|
let trust;
|
|
22203
22986
|
try {
|
|
22204
|
-
const metadata =
|
|
22987
|
+
const metadata = lstatSync18(options.trustFile);
|
|
22205
22988
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > 131072) throw new Error();
|
|
22206
|
-
const bytes =
|
|
22989
|
+
const bytes = readFileSync22(options.trustFile);
|
|
22207
22990
|
if (bytes.length > 131072) throw new Error();
|
|
22208
22991
|
trust = JSON.parse(new TextDecoder("utf8", { fatal: true }).decode(bytes));
|
|
22209
22992
|
} catch {
|
|
@@ -22213,14 +22996,14 @@ function workloadTrustState(options) {
|
|
|
22213
22996
|
if (trust.revision !== options.expectedRevision) throw new Error("Application identity trust revision changed; review again");
|
|
22214
22997
|
if (options.action === "initialize") {
|
|
22215
22998
|
if (!options.confirmed) throw new Error("Initialization requires explicit --confirm after reviewing the plan");
|
|
22216
|
-
initializeApplicationAuthState(trust, { expectedScope: options.expectedScope, stateFile:
|
|
22999
|
+
initializeApplicationAuthState(trust, { expectedScope: options.expectedScope, stateFile: resolve22(options.stateFile) });
|
|
22217
23000
|
}
|
|
22218
|
-
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 };
|
|
22219
23002
|
}
|
|
22220
23003
|
|
|
22221
23004
|
// src/scaffold.ts
|
|
22222
|
-
import { existsSync as
|
|
22223
|
-
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";
|
|
22224
23007
|
function json2(value) {
|
|
22225
23008
|
return JSON.stringify(value, null, 2) + "\n";
|
|
22226
23009
|
}
|
|
@@ -22390,521 +23173,86 @@ Packaging requires an explicitly approved digest-pinned base image. Hosted durab
|
|
|
22390
23173
|
dispatch/scheduling is not implemented by this template. Do not directly execute
|
|
22391
23174
|
server/jobs.ts; it is a declaration-only entry loaded by a trusted Backify host.
|
|
22392
23175
|
The Dockerfile builds the HTTP server and web/dist frontend and serves both from
|
|
22393
|
-
one origin, with HOST/PORT from its host. Keep package-lock.json in source control.
|
|
22394
|
-
Use backify ship for Preview and promote that tested release to Live. No production
|
|
22395
|
-
provider-readiness claim, cloud deployment or browser authentication is implied.
|
|
22396
|
-
`
|
|
22397
|
-
};
|
|
22398
|
-
}
|
|
22399
|
-
function scaffoldFullStack(options) {
|
|
22400
|
-
const files = fullStackTemplate(options.appId, options.name);
|
|
22401
|
-
const root = resolve21(options.root ?? process.cwd());
|
|
22402
|
-
if (!existsSync11(root) || !lstatSync17(root).isDirectory() || lstatSync17(root).isSymbolicLink()) throw new Error("Scaffold destination must be an existing non-linked directory");
|
|
22403
|
-
const allowed = /* @__PURE__ */ new Set([...Object.keys(files).map((filename2) => filename2.split("/")[0]), ".git", "node_modules", "package-lock.json", ".backify"]);
|
|
22404
|
-
if (readdirSync6(root).some((filename2) => !allowed.has(filename2))) throw new Error("Scaffolding requires an empty folder or an unchanged generated starter");
|
|
22405
|
-
const canonicalRoot = realpathSync8(root);
|
|
22406
|
-
const pending = [];
|
|
22407
|
-
for (const [filename2, content] of Object.entries(files)) {
|
|
22408
|
-
const destination = join13(root, filename2);
|
|
22409
|
-
let current = destination;
|
|
22410
|
-
while (current !== root) {
|
|
22411
|
-
try {
|
|
22412
|
-
const stat = lstatSync17(current);
|
|
22413
|
-
if (stat.isSymbolicLink() || current !== destination && !stat.isDirectory()) throw new Error("Scaffolding refuses linked or invalid output paths: " + filename2);
|
|
22414
|
-
if (relative10(canonicalRoot, realpathSync8(current)).startsWith("..")) throw new Error("Scaffold output must remain inside its directory");
|
|
22415
|
-
if (current === destination && (!stat.isFile() || readFileSync20(current, "utf8") !== content)) throw new Error("Scaffolding refuses to overwrite existing content: " + filename2);
|
|
22416
|
-
} catch (error) {
|
|
22417
|
-
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
|
|
22418
|
-
if (current === destination) pending.push(filename2);
|
|
22419
|
-
}
|
|
22420
|
-
current = dirname13(current);
|
|
22421
|
-
}
|
|
22422
|
-
}
|
|
22423
|
-
for (const filename2 of pending) {
|
|
22424
|
-
const destination = join13(root, filename2);
|
|
22425
|
-
mkdirSync8(dirname13(destination), { recursive: true });
|
|
22426
|
-
writeFileSync10(destination, files[filename2], { flag: "wx" });
|
|
22427
|
-
}
|
|
22428
|
-
return { template: "react-vite-hono", created: pending, unchanged: Object.keys(files).length - pending.length, installed: false, deployed: false };
|
|
22429
|
-
}
|
|
22430
|
-
|
|
22431
|
-
// src/local-inspection-command.ts
|
|
22432
|
-
function localInspectionCommand(args2) {
|
|
22433
|
-
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" };
|
|
22434
|
-
const kind = args2[0];
|
|
22435
|
-
if (!Object.hasOwn(routes, kind)) throw new Error("Use backify inspect tables|rows|buckets|objects|preview|events --local [--json]");
|
|
22436
|
-
const allowed = kind === "rows" ? ["--table", "--limit", "--offset"] : kind === "objects" ? ["--bucket", "--prefix", "--limit", "--offset"] : kind === "preview" ? ["--bucket", "--key"] : [];
|
|
22437
|
-
const values = /* @__PURE__ */ new Map();
|
|
22438
|
-
const flags = /* @__PURE__ */ new Set();
|
|
22439
|
-
for (let index = 1; index < args2.length; index++) {
|
|
22440
|
-
const name2 = args2[index];
|
|
22441
|
-
if (["--local", "--json"].includes(name2)) {
|
|
22442
|
-
if (flags.has(name2)) throw new Error("Duplicate inspection flag");
|
|
22443
|
-
flags.add(name2);
|
|
22444
|
-
continue;
|
|
22445
|
-
}
|
|
22446
|
-
if (!allowed.includes(name2) || values.has(name2) || args2[index + 1] === void 0 || args2[index + 1].startsWith("--")) throw new Error("Invalid or missing inspection option");
|
|
22447
|
-
values.set(name2, args2[++index]);
|
|
22448
|
-
}
|
|
22449
|
-
if (!flags.has("--local")) throw new Error("Inspection requires --local; no cloud or production data is queried");
|
|
22450
|
-
const required = (name2) => {
|
|
22451
|
-
const value = values.get(name2);
|
|
22452
|
-
if (!value) throw new Error("Required inspection option: " + name2);
|
|
22453
|
-
return value;
|
|
22454
|
-
};
|
|
22455
|
-
const page = () => {
|
|
22456
|
-
const number = (name2, fallback, minimum, maximum) => {
|
|
22457
|
-
const value = values.get(name2);
|
|
22458
|
-
if (value !== void 0 && !/^\d+$/.test(value)) throw new Error("Invalid inspection page");
|
|
22459
|
-
const parsed = value === void 0 ? fallback : Number(value);
|
|
22460
|
-
if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) throw new Error("Invalid inspection page");
|
|
22461
|
-
return parsed;
|
|
22462
|
-
};
|
|
22463
|
-
return { limit: number("--limit", 25, 1, 50), offset: number("--offset", 0, 0, 1e4) };
|
|
22464
|
-
};
|
|
22465
|
-
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;
|
|
22466
|
-
return { kind, path: routes[kind], input };
|
|
22467
|
-
}
|
|
22468
|
-
|
|
22469
|
-
// src/development.ts
|
|
22470
|
-
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";
|
|
22471
|
-
import { dirname as dirname16, join as join16, resolve as resolve23 } from "node:path";
|
|
22472
|
-
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
22473
|
-
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
22474
|
-
|
|
22475
|
-
// src/setup.ts
|
|
22476
|
-
import { existsSync as existsSync13, readFileSync as readFileSync22, writeFileSync as writeFileSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync7, lstatSync as lstatSync18 } from "node:fs";
|
|
22477
|
-
import { basename as basename3, dirname as dirname15, join as join15, resolve as resolve22, isAbsolute as isAbsolute16 } from "node:path";
|
|
22478
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
22479
|
-
|
|
22480
|
-
// src/delivery.ts
|
|
22481
|
-
import { existsSync as existsSync12, readFileSync as readFileSync21, writeFileSync as writeFileSync11 } from "node:fs";
|
|
22482
|
-
import { dirname as dirname14, join as join14, relative as relativePath } from "node:path";
|
|
22483
|
-
import { createHash as createHash15 } from "node:crypto";
|
|
22484
|
-
var previousGeneratedWorkflowHashes = /* @__PURE__ */ new Set(["33e962274e8493f7cbac2afbac05a4e96afeec49ee08cf2bf82f8add9081b28b", "578dfb4c771c623fd04454e8a604efd701d5563d11e11191324862369bd24718", "6eb280ae7bdb14c390011686c41400f59c023304c3015d8440b7e80aaafa1c11", "545e03a1d5b996ade179e0e60ed5c86accdadc1982c1c25efd094e34f72f47ee"]);
|
|
22485
|
-
function validateDeliveryInstructions(root) {
|
|
22486
|
-
for (const filename2 of ["BACKIFY.md", "BACKIFY-WORKFLOW.md"]) {
|
|
22487
|
-
const path = join14(root, filename2);
|
|
22488
|
-
if (existsSync12(path)) {
|
|
22489
|
-
const existing = readFileSync21(path, "utf8").replace(/\r\n/g, "\n");
|
|
22490
|
-
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.");
|
|
22491
|
-
}
|
|
22492
|
-
}
|
|
22493
|
-
}
|
|
22494
|
-
function installDeliveryInstructions(root, instructionFiles2) {
|
|
22495
|
-
validateDeliveryInstructions(root);
|
|
22496
|
-
writeFileSync11(join14(root, "BACKIFY.md"), deliveryInstructions);
|
|
22497
|
-
for (const relative11 of instructionFiles2) {
|
|
22498
|
-
const instructionPath = join14(root, relative11), content = readFileSync21(instructionPath, "utf8");
|
|
22499
|
-
const link = relativePath(dirname14(instructionPath), join14(root, "BACKIFY.md")).replaceAll("\\", "/");
|
|
22500
|
-
const legacyLink = relativePath(dirname14(instructionPath), join14(root, "BACKIFY-WORKFLOW.md")).replaceAll("\\", "/");
|
|
22501
|
-
const legacy = "For application delivery and recording each user request, follow [BACKIFY-WORKFLOW.md](" + legacyLink + ").";
|
|
22502
|
-
const guidance = "For application delivery and recording each user request, follow [BACKIFY.md](" + link + ").";
|
|
22503
|
-
if (content.includes(legacy)) writeFileSync11(instructionPath, content.replace(legacy, guidance));
|
|
22504
|
-
else if (!content.includes("## Backify delivery")) writeFileSync11(instructionPath, content.trimEnd() + "\n\n## Backify delivery\n\n" + guidance + "\n");
|
|
22505
|
-
}
|
|
22506
|
-
}
|
|
22507
|
-
|
|
22508
|
-
// src/setup.ts
|
|
22509
|
-
var instructionFiles = ["AGENTS.md"];
|
|
22510
|
-
var agentInstructionFiles = { codex: "AGENTS.md", claude: "CLAUDE.md", copilot: join15(".github", "copilot-instructions.md"), gemini: "GEMINI.md" };
|
|
22511
|
-
function instructionSelection(root, agent, instructionFile) {
|
|
22512
|
-
const configPath2 = join15(root, "backify.json"), configured = existsSync13(configPath2) ? JSON.parse(readFileSync22(configPath2, "utf8")) : {};
|
|
22513
|
-
const custom = instructionFile ?? (agent === void 0 ? configured.instructionFile : void 0);
|
|
22514
|
-
const selected = instructionFile !== void 0 ? "custom" : agent ?? configured.agent ?? "codex";
|
|
22515
|
-
if (typeof selected !== "string" || selected !== "custom" && !Object.hasOwn(agentInstructionFiles, selected)) throw new Error("Select the current tool with --agent or --instruction-file <relative-path>.");
|
|
22516
|
-
const filename2 = custom ?? agentInstructionFiles[selected];
|
|
22517
|
-
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.");
|
|
22518
|
-
const normalized = filename2.replaceAll("\\", "/"), parts = normalized.split("/");
|
|
22519
|
-
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.");
|
|
22520
|
-
for (let index = 1; index <= parts.length; index++) {
|
|
22521
|
-
const entry = join15(root, ...parts.slice(0, index));
|
|
22522
|
-
try {
|
|
22523
|
-
const metadata = lstatSync18(entry);
|
|
22524
|
-
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.");
|
|
22525
|
-
} catch (reason) {
|
|
22526
|
-
if (reason.code !== "ENOENT") throw reason;
|
|
22527
|
-
}
|
|
22528
|
-
}
|
|
22529
|
-
return { agent: selected, instructionFile: normalized, files: [normalized] };
|
|
22530
|
-
}
|
|
22531
|
-
function selectedInstructionFiles(root = process.cwd(), agent, instructionFile) {
|
|
22532
|
-
return instructionSelection(root, agent, instructionFile).files;
|
|
22533
|
-
}
|
|
22534
|
-
function isToolWorkspaceEntry(entry, root = process.cwd()) {
|
|
22535
|
-
if (entry.name.toLowerCase() === ".git" || entry.name.toLowerCase() === ".env" || entry.name.toLowerCase().startsWith(".env.")) return false;
|
|
22536
|
-
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));
|
|
22537
|
-
}
|
|
22538
|
-
function detectPackageManager(root = process.cwd()) {
|
|
22539
|
-
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;
|
|
22540
|
-
return "npm";
|
|
22541
|
-
}
|
|
22542
|
-
function updateInstructions(root = process.cwd(), agent, instructionFile) {
|
|
22543
|
-
const updates = selectedInstructionFiles(root, agent, instructionFile).map((relative11) => {
|
|
22544
|
-
const path = join15(root, relative11);
|
|
22545
|
-
return { path, content: updateManagedInstructions(existsSync13(path) ? readFileSync22(path, "utf8") : "") };
|
|
22546
|
-
});
|
|
22547
|
-
for (const { path, content } of updates) {
|
|
22548
|
-
mkdirSync9(dirname15(path), { recursive: true });
|
|
22549
|
-
writeFileSync12(path, content);
|
|
22550
|
-
}
|
|
22551
|
-
}
|
|
22552
|
-
function synchronize(root = process.cwd(), agent, instructionFile) {
|
|
22553
|
-
const path = join15(root, "backify.json");
|
|
22554
|
-
if (!existsSync13(path)) throw new Error("backify.json is missing; run backify init --app app_xxxxx");
|
|
22555
|
-
const value = JSON.parse(readFileSync22(path, "utf8")), issues = validateConfig(value);
|
|
22556
|
-
if (issues.length) throw new Error(issues.join("; "));
|
|
22557
|
-
if (value.instructionVersion > INSTRUCTION_VERSION) throw new Error("This app uses newer instructions. Install a matching or newer CLI before syncing.");
|
|
22558
|
-
const selected = instructionSelection(root, agent, instructionFile);
|
|
22559
|
-
validateDeliveryInstructions(root);
|
|
22560
|
-
updateInstructions(root, selected.agent, selected.instructionFile);
|
|
22561
|
-
installDeliveryInstructions(root, selected.files);
|
|
22562
|
-
value.agent = selected.agent;
|
|
22563
|
-
value.instructionFile = selected.instructionFile;
|
|
22564
|
-
value.instructionVersion = INSTRUCTION_VERSION;
|
|
22565
|
-
writeFileSync12(path, JSON.stringify(value, null, 2) + "\n");
|
|
22566
|
-
}
|
|
22567
|
-
function sdkInstalled(root = process.cwd()) {
|
|
22568
|
-
try {
|
|
22569
|
-
const pkg = JSON.parse(readFileSync22(join15(root, "package.json"), "utf8"));
|
|
22570
|
-
return Boolean(pkg.dependencies?.[SDK_PACKAGE] || pkg.devDependencies?.[SDK_PACKAGE]);
|
|
22571
|
-
} catch {
|
|
22572
|
-
return false;
|
|
22573
|
-
}
|
|
22574
|
-
}
|
|
22575
|
-
function installSdk(root = process.cwd()) {
|
|
22576
|
-
if (sdkInstalled(root)) return;
|
|
22577
|
-
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" });
|
|
22578
|
-
if (result.status !== 0) throw new Error(`Could not install ${SDK_PACKAGE} with ${manager}`);
|
|
22579
|
-
}
|
|
22580
|
-
function ensurePackageManifest(root) {
|
|
22581
|
-
const path = join15(root, "package.json");
|
|
22582
|
-
if (existsSync13(path)) return;
|
|
22583
|
-
const setupFiles = /* @__PURE__ */ new Set([".git", ".gitignore", ".github", "AGENTS.md", "CLAUDE.md", "GEMINI.md", "BACKIFY.md", "BACKIFY-WORKFLOW.md", "README.md", "LICENSE", "backify.json"]);
|
|
22584
|
-
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/");
|
|
22585
|
-
const name2 = basename3(resolve22(root)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "backify-app";
|
|
22586
|
-
writeFileSync12(path, JSON.stringify({ name: name2, version: "0.0.0", private: true, type: "module" }, null, 2) + "\n", { flag: "wx" });
|
|
22587
|
-
}
|
|
22588
|
-
function initialize(appId, root = process.cwd(), install = true, agent, instructionFile) {
|
|
22589
|
-
if (!/^app_[A-Za-z0-9_-]+$/.test(appId)) throw new Error("Provide an application ID using --app app_xxxxx");
|
|
22590
|
-
const selected = instructionSelection(root, agent, instructionFile), files = selected.files;
|
|
22591
|
-
validateDeliveryInstructions(root);
|
|
22592
|
-
for (const relative11 of files) if (existsSync13(join15(root, relative11))) updateManagedInstructions(readFileSync22(join15(root, relative11), "utf8"));
|
|
22593
|
-
ensurePackageManifest(root);
|
|
22594
|
-
if (install) installSdk(root);
|
|
22595
|
-
const configPath2 = join15(root, "backify.json"), existing = existsSync13(configPath2) ? JSON.parse(readFileSync22(configPath2, "utf8")) : {};
|
|
22596
|
-
writeFileSync12(configPath2, JSON.stringify({ ...existing, ...createConfig(appId), agent: selected.agent, instructionFile: selected.instructionFile }, null, 2) + "\n");
|
|
22597
|
-
updateInstructions(root, selected.agent, selected.instructionFile);
|
|
22598
|
-
installDeliveryInstructions(root, files);
|
|
22599
|
-
}
|
|
22600
|
-
function doctor(root = process.cwd(), agent, instructionFile) {
|
|
22601
|
-
const issues = [], warnings = [];
|
|
22602
|
-
if (!sdkInstalled(root)) issues.push(`${SDK_PACKAGE} is not installed`);
|
|
22603
|
-
let config;
|
|
22604
|
-
try {
|
|
22605
|
-
config = JSON.parse(readFileSync22(join15(root, "backify.json"), "utf8"));
|
|
22606
|
-
issues.push(...validateConfig(config));
|
|
22607
|
-
} catch {
|
|
22608
|
-
issues.push("backify.json is missing or invalid JSON");
|
|
22609
|
-
}
|
|
22610
|
-
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.`);
|
|
22611
|
-
let files = instructionFiles;
|
|
22612
|
-
try {
|
|
22613
|
-
files = selectedInstructionFiles(root, agent, instructionFile);
|
|
22614
|
-
} catch (reason) {
|
|
22615
|
-
issues.push(reason instanceof Error ? reason.message : String(reason));
|
|
22616
|
-
}
|
|
22617
|
-
for (const relative11 of files) {
|
|
22618
|
-
const path = join15(root, relative11);
|
|
22619
|
-
if (!existsSync13(path)) {
|
|
22620
|
-
issues.push(`${relative11} is missing`);
|
|
22621
|
-
continue;
|
|
22622
|
-
}
|
|
22623
|
-
issues.push(...managedInstructionIssues(readFileSync22(path, "utf8")).map((issue) => `${relative11} ${issue}`));
|
|
22624
|
-
}
|
|
22625
|
-
let version = "unknown";
|
|
22626
|
-
try {
|
|
22627
|
-
const pkg = JSON.parse(readFileSync22(join15(root, "package.json"), "utf8"));
|
|
22628
|
-
version = pkg.dependencies?.[SDK_PACKAGE] ?? pkg.devDependencies?.[SDK_PACKAGE] ?? "missing";
|
|
22629
|
-
} catch {
|
|
22630
|
-
}
|
|
22631
|
-
return { valid: issues.length === 0, appId: config?.appId, sdkVersion: version, instructionVersion: config?.instructionVersion, currentInstructionVersion: INSTRUCTION_VERSION, issues, warnings };
|
|
22632
|
-
}
|
|
22633
|
-
|
|
22634
|
-
// src/development.ts
|
|
22635
|
-
function promptMetadata(args2) {
|
|
22636
|
-
const fields5 = [["--model", "model", 200], ["--ai-tool", "aiTool", 200], ["--access-level", "accessLevel", 500]];
|
|
22637
|
-
const metadata = {};
|
|
22638
|
-
for (const [flag, field, maxLength] of fields5) {
|
|
22639
|
-
const index = args2.indexOf(flag);
|
|
22640
|
-
if (index < 0) continue;
|
|
22641
|
-
const value = args2[index + 1]?.trim();
|
|
22642
|
-
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))
|
|
22643
|
-
throw new Error(`Provide ${flag} with a nonempty value of at most ${maxLength} characters without control characters or credentials.`);
|
|
22644
|
-
metadata[field] = value;
|
|
22645
|
-
}
|
|
22646
|
-
return metadata;
|
|
22647
|
-
}
|
|
22648
|
-
function applicationGuid(appId) {
|
|
22649
|
-
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();
|
|
22650
|
-
const match = /^app_([0-9a-f]{32})$/i.exec(appId);
|
|
22651
|
-
if (!match) throw new Error("Use the application ID supplied by Backify.");
|
|
22652
|
-
const value = match[1];
|
|
22653
|
-
return [value.slice(0, 8), value.slice(8, 12), value.slice(12, 16), value.slice(16, 20), value.slice(20)].join("-");
|
|
22654
|
-
}
|
|
22655
|
-
function projectApplication(root = process.cwd()) {
|
|
22656
|
-
if (!existsSync14(join16(root, "backify.json"))) throw new Error("Run backify connect --app <application-id>, then backify init.");
|
|
22657
|
-
return JSON.parse(readFileSync23(join16(root, "backify.json"), "utf8")).appId;
|
|
22658
|
-
}
|
|
22659
|
-
function apiAddress(value) {
|
|
22660
|
-
const address = new URL(value);
|
|
22661
|
-
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).");
|
|
22662
|
-
return address.toString().replace(/\/$/, "");
|
|
23176
|
+
one origin, with HOST/PORT from its host. Keep package-lock.json in source control.
|
|
23177
|
+
Use backify ship for Preview and promote that tested release to Live. No production
|
|
23178
|
+
provider-readiness claim, cloud deployment or browser authentication is implied.
|
|
23179
|
+
`
|
|
23180
|
+
};
|
|
22663
23181
|
}
|
|
22664
|
-
function
|
|
22665
|
-
const
|
|
22666
|
-
|
|
22667
|
-
|
|
22668
|
-
|
|
22669
|
-
|
|
22670
|
-
|
|
22671
|
-
|
|
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) {
|
|
22672
23194
|
try {
|
|
22673
|
-
const
|
|
22674
|
-
|
|
22675
|
-
|
|
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);
|
|
22676
23202
|
}
|
|
22677
|
-
|
|
22678
|
-
}
|
|
22679
|
-
return response.status === 204 ? void 0 : await response.json();
|
|
22680
|
-
};
|
|
22681
|
-
}
|
|
22682
|
-
function saveUserConfig(path, config) {
|
|
22683
|
-
const temporary = path + "." + randomUUID9() + ".tmp";
|
|
22684
|
-
try {
|
|
22685
|
-
writeFileSync13(temporary, JSON.stringify(config, null, 2) + "\n", { mode: 384, flag: "wx" });
|
|
22686
|
-
if (process.platform === "win32") {
|
|
22687
|
-
const account = [process.env.USERDOMAIN, process.env.USERNAME].filter(Boolean).join("\\");
|
|
22688
|
-
if (!account) throw new Error("Cannot identify the account to protect CLI credentials.");
|
|
22689
|
-
const secured = spawnSync3("icacls", [temporary, "/inheritance:r", "/grant:r", account + ":(F)"], { windowsHide: true, encoding: "utf8" });
|
|
22690
|
-
if (secured.status !== 0) throw new Error("Could not restrict CLI credential file permissions.");
|
|
22691
|
-
}
|
|
22692
|
-
renameSync4(temporary, path);
|
|
22693
|
-
} finally {
|
|
22694
|
-
rmSync2(temporary, { force: true });
|
|
22695
|
-
}
|
|
22696
|
-
}
|
|
22697
|
-
function repositoryName(remote) {
|
|
22698
|
-
const match = /^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?\/?$/i.exec(remote);
|
|
22699
|
-
if (!match) throw new Error("Repository must be a credential-free GitHub URL.");
|
|
22700
|
-
return match[1].toLowerCase();
|
|
22701
|
-
}
|
|
22702
|
-
function git2(args2, root = process.cwd(), environment = process.env) {
|
|
22703
|
-
const result = spawnSync3("git", args2, { cwd: root, env: environment, encoding: "utf8", windowsHide: true, maxBuffer: 2 * 1024 * 1024 });
|
|
22704
|
-
if (result.status !== 0) throw new Error("Git " + args2[0] + " failed. Check repository access, branch protection, and local changes.");
|
|
22705
|
-
return result.stdout.trim();
|
|
22706
|
-
}
|
|
22707
|
-
function gitEnvironment(repository, token) {
|
|
22708
|
-
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) throw new Error("Invalid repository.");
|
|
22709
|
-
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" };
|
|
22710
|
-
}
|
|
22711
|
-
function validateCloneMerge(source, target) {
|
|
22712
|
-
for (const entry of readdirSync8(source, { withFileTypes: true })) {
|
|
22713
|
-
const destination = join16(target, entry.name);
|
|
22714
|
-
let existing;
|
|
22715
|
-
try {
|
|
22716
|
-
existing = lstatSync19(destination);
|
|
22717
|
-
} catch (reason) {
|
|
22718
|
-
if (reason.code === "ENOENT") continue;
|
|
22719
|
-
throw reason;
|
|
23203
|
+
current = dirname16(current);
|
|
22720
23204
|
}
|
|
22721
|
-
if (entry.isDirectory() && existing.isDirectory() && !existing.isSymbolicLink()) validateCloneMerge(join16(source, entry.name), destination);
|
|
22722
|
-
else throw new Error("Clone conflicts with existing workspace entry: " + destination + ". Existing files are never overwritten; reconcile the conflict or use a new folder.");
|
|
22723
|
-
}
|
|
22724
|
-
}
|
|
22725
|
-
async function cloneProject(call2, appId, root = process.cwd()) {
|
|
22726
|
-
const status = await call2("/apps/" + applicationGuid(appId) + "/developer");
|
|
22727
|
-
if (!status.repository || !status.branch || !status.workflowReady) throw new Error("Connect the repository in Backify and wait for workflow setup.");
|
|
22728
|
-
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.");
|
|
22729
|
-
const credential = await call2("/apps/" + applicationGuid(appId) + "/developer/git-credential", { method: "POST" });
|
|
22730
|
-
if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed. Run connect again.");
|
|
22731
|
-
const staging = mkdtempSync2(join16(dirname16(resolve23(root)), ".backify-clone-"));
|
|
22732
|
-
try {
|
|
22733
|
-
git2(["clone", "--single-branch", "--branch", status.branch, "--", "https://github.com/" + status.repository + ".git", "."], staging, gitEnvironment(status.repository, credential.token));
|
|
22734
|
-
validateCloneMerge(staging, root);
|
|
22735
|
-
for (const entry of readdirSync8(staging)) cpSync(join16(staging, entry), join16(root, entry), { recursive: true, force: false, errorOnExist: true, dereference: false });
|
|
22736
|
-
} finally {
|
|
22737
|
-
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.");
|
|
22738
|
-
rmSync2(staging, { recursive: true, force: true });
|
|
22739
|
-
}
|
|
22740
|
-
}
|
|
22741
|
-
async function pullProject(call2, appId, root = process.cwd()) {
|
|
22742
|
-
if (existsSync14(join16(root, "backify.json")) && applicationGuid(projectApplication(root)) !== applicationGuid(appId)) throw new Error("Local application ID does not match the connected application.");
|
|
22743
|
-
const prefix = "/apps/" + applicationGuid(appId);
|
|
22744
|
-
const status = await call2(prefix + "/developer");
|
|
22745
|
-
if (!status.repository || !status.branch) throw new Error("Connect the repository and branch in Backify before pulling.");
|
|
22746
|
-
if (resolve23(git2(["rev-parse", "--show-toplevel"], root)) !== resolve23(root)) throw new Error("Run pull from the connected repository root.");
|
|
22747
|
-
if (repositoryName(git2(["remote", "get-url", "origin"], root)) !== status.repository.toLowerCase()) throw new Error("Local origin does not match the repository connected in Backify.");
|
|
22748
|
-
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.");
|
|
22749
|
-
git2(["check-ref-format", "--branch", status.branch], root);
|
|
22750
|
-
const credential = await call2(prefix + "/developer/git-credential", { method: "POST" });
|
|
22751
|
-
if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed; reconnect before pulling.");
|
|
22752
|
-
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));
|
|
22753
|
-
const remoteCommit = git2(["rev-parse", "FETCH_HEAD"], root);
|
|
22754
|
-
const counts = git2(["rev-list", "--left-right", "--count", "HEAD..." + remoteCommit], root).split(/\s+/).map(Number);
|
|
22755
|
-
if (counts.length !== 2 || counts.some((count) => !Number.isSafeInteger(count) || count < 0)) throw new Error("Could not compare local and remote history.");
|
|
22756
|
-
const [ahead, behind] = counts;
|
|
22757
|
-
if (!behind) return { status: "up-to-date", ahead, behind, commit: git2(["rev-parse", "HEAD"], root) };
|
|
22758
|
-
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 };
|
|
22759
|
-
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 };
|
|
22760
|
-
git2(["merge", "--ff-only", "--", remoteCommit], root);
|
|
22761
|
-
return { status: "updated", ahead: 0, behind: 0, commit: git2(["rev-parse", "HEAD"], root) };
|
|
22762
|
-
}
|
|
22763
|
-
async function readPrompt(input) {
|
|
22764
|
-
const chunks = [];
|
|
22765
|
-
let size = 0;
|
|
22766
|
-
for await (const chunk of input) {
|
|
22767
|
-
const bytes = Buffer.from(chunk);
|
|
22768
|
-
size += bytes.length;
|
|
22769
|
-
if (size > 128e3) throw new Error("Prompt exceeds the size limit.");
|
|
22770
|
-
chunks.push(bytes);
|
|
22771
|
-
}
|
|
22772
|
-
const text3 = Buffer.concat(chunks).toString("utf8");
|
|
22773
|
-
if (!text3.trim() || text3.length > 32e3) throw new Error("Provide between 1 and 32000 characters of user prompt text.");
|
|
22774
|
-
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.");
|
|
22775
|
-
return text3;
|
|
22776
|
-
}
|
|
22777
|
-
function attachmentNames(values) {
|
|
22778
|
-
if (values.length > 50) throw new Error("At most 50 attachment filenames are allowed.");
|
|
22779
|
-
return [...new Set(values.map((value) => {
|
|
22780
|
-
const filename2 = value.replace(/\\/g, "/").split("/").pop();
|
|
22781
|
-
if (!filename2 || filename2 === "." || filename2 === ".." || filename2.length > 255 || /[\x00-\x1f\x7f]/.test(filename2)) throw new Error("Invalid attachment filename.");
|
|
22782
|
-
return filename2;
|
|
22783
|
-
}))];
|
|
22784
|
-
}
|
|
22785
|
-
function localReport(runChecks, root = process.cwd()) {
|
|
22786
|
-
const pkg = JSON.parse(readFileSync23(join16(root, "package.json"), "utf8"));
|
|
22787
|
-
const dependencies = { ...pkg.devDependencies, ...pkg.dependencies };
|
|
22788
|
-
const framework = ["next", "vite", "astro", "@sveltejs/kit", "nuxt", "react", "express"].find((name2) => dependencies[name2]) ?? "unknown";
|
|
22789
|
-
const setup = doctor(root);
|
|
22790
|
-
const checks = [{ name: "backify doctor", status: setup.valid ? "passed" : "failed" }];
|
|
22791
|
-
const manager = detectPackageManager(root);
|
|
22792
|
-
for (const name2 of ["typecheck", "lint", "test", "build"]) {
|
|
22793
|
-
if (!pkg.scripts?.[name2]) continue;
|
|
22794
|
-
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;
|
|
22795
|
-
checks.push({ name: name2, status: result ? result.status === 0 ? "passed" : "failed" : "not-run" });
|
|
22796
|
-
}
|
|
22797
|
-
let branch = null, commitSha = null, dirty = true;
|
|
22798
|
-
try {
|
|
22799
|
-
branch = git2(["branch", "--show-current"], root);
|
|
22800
|
-
commitSha = git2(["rev-parse", "HEAD"], root);
|
|
22801
|
-
dirty = Boolean(git2(["status", "--porcelain"], root));
|
|
22802
|
-
} catch {
|
|
22803
|
-
}
|
|
22804
|
-
const outputDirectory = ["dist", "build", "out", "."].find((directory2) => existsSync14(join16(root, directory2, "index.html"))) ?? null;
|
|
22805
|
-
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 };
|
|
22806
|
-
}
|
|
22807
|
-
function matchesSource(release, commit) {
|
|
22808
|
-
return release?.commitSha === commit || release?.sourceCommitSha === commit;
|
|
22809
|
-
}
|
|
22810
|
-
function pushSource(appId, commit, branch, destination, root, environment) {
|
|
22811
|
-
const remoteHead = git2(["ls-remote", "--", destination, "refs/heads/" + branch], root, environment).split(/\s+/)[0];
|
|
22812
|
-
if (remoteHead === commit) return true;
|
|
22813
|
-
if (remoteHead) {
|
|
22814
|
-
git2(["fetch", "--no-tags", "--", destination, "refs/heads/" + branch], root, environment);
|
|
22815
|
-
const fetchedHead = git2(["rev-parse", "FETCH_HEAD"], root);
|
|
22816
|
-
if (fetchedHead === commit) return true;
|
|
22817
|
-
const parent = git2(["show", "-s", "--format=%P", fetchedHead], root);
|
|
22818
|
-
const message = git2(["show", "-s", "--format=%B", fetchedHead], root);
|
|
22819
|
-
if (parent === commit && message === "Backify build " + applicationGuid(appId).replaceAll("-", "") + ": " + commit) return true;
|
|
22820
|
-
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.");
|
|
22821
23205
|
}
|
|
22822
|
-
|
|
22823
|
-
|
|
22824
|
-
}
|
|
22825
|
-
|
|
22826
|
-
const deadline = Date.now() + (options.timeoutMs ?? 20 * 6e4);
|
|
22827
|
-
const sleep = options.sleep ?? ((delay) => new Promise((resolve24) => setTimeout(resolve24, delay)));
|
|
22828
|
-
do {
|
|
22829
|
-
const deployments = await call2(prefix + "/deployments");
|
|
22830
|
-
const matching = deployments.find((release) => release.kind === "preview" && matchesSource(release, commit) && !options.previousDeploymentIds?.includes(release.id));
|
|
22831
|
-
if (matching?.status === "failed") throw new Error("Deployment of this source failed. Run backify status and inspect its deployment steps.");
|
|
22832
|
-
const overview = await call2(prefix + "/releases");
|
|
22833
|
-
if (matchesSource(overview.preview, commit) && overview.preview?.status === "succeeded" && overview.preview.healthResult === "healthy") return overview;
|
|
22834
|
-
const build3 = await call2(prefix + "/developer/build?commitSha=" + commit);
|
|
22835
|
-
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."));
|
|
22836
|
-
await sleep(options.intervalMs ?? 4e3);
|
|
22837
|
-
} while (Date.now() < deadline);
|
|
22838
|
-
throw new Error("Timed out waiting for this commit's healthy preview. Run backify status; no production release was requested.");
|
|
22839
|
-
}
|
|
22840
|
-
async function ship(call2, appId, options = {}) {
|
|
22841
|
-
const root = options.root ?? process.cwd(), prefix = "/apps/" + applicationGuid(appId);
|
|
22842
|
-
const status = await call2(prefix + "/developer");
|
|
22843
|
-
if (!status.repository || !status.branch || !status.workflowReady) throw new Error("The application's repository and build workflow must be connected in Backify.");
|
|
22844
|
-
if (!doctor(root).valid) throw new Error("Run backify doctor and fix project setup before shipping.");
|
|
22845
|
-
if (applicationGuid(projectApplication(root)) !== applicationGuid(appId)) throw new Error("Local application ID does not match the application being shipped.");
|
|
22846
|
-
const actualRoot = git2(["rev-parse", "--show-toplevel"], root);
|
|
22847
|
-
if (resolve23(actualRoot).toLowerCase() !== resolve23(root).toLowerCase()) throw new Error("Shipping currently requires the application at the repository root.");
|
|
22848
|
-
const origin = git2(["remote", "get-url", "origin"], root);
|
|
22849
|
-
const pushOrigin = git2(["remote", "get-url", "--push", "origin"], root);
|
|
22850
|
-
if (repositoryName(origin) !== status.repository.toLowerCase() || repositoryName(pushOrigin) !== status.repository.toLowerCase()) throw new Error("Local repository does not match this Backify application.");
|
|
22851
|
-
if (git2(["branch", "--show-current"], root) !== status.branch) throw new Error("Use the connected branch: " + status.branch);
|
|
22852
|
-
if (git2(["status", "--porcelain"], root)) throw new Error("Commit application changes before shipping. The CLI never stages or commits unrelated files.");
|
|
22853
|
-
const commit = git2(["rev-parse", "HEAD"], root);
|
|
22854
|
-
const report = localReport(true, root);
|
|
22855
|
-
await call2(prefix + "/developer/report", { method: "POST", body: JSON.stringify(report) });
|
|
22856
|
-
if (report.checks.some((check) => check.status === "failed")) throw new Error("Checks failed; nothing was pushed.");
|
|
22857
|
-
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.");
|
|
22858
|
-
if (status.hostingTargetType === "azure" && !report.hasDockerfile) throw new Error("Customer Azure requires a repository-root Dockerfile.");
|
|
22859
|
-
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.");
|
|
22860
|
-
const previousDeployments = await call2(prefix + "/deployments");
|
|
22861
|
-
const previousBuild = await call2(prefix + "/developer/build?commitSha=" + commit);
|
|
22862
|
-
const credential = await call2(prefix + "/developer/git-credential", { method: "POST" });
|
|
22863
|
-
if (credential.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed; reconnect before shipping.");
|
|
22864
|
-
const environment = gitEnvironment(status.repository, credential.token);
|
|
22865
|
-
const destination = "https://github.com/" + status.repository + ".git";
|
|
22866
|
-
const alreadyPushed = pushSource(appId, commit, status.branch, destination, root, environment);
|
|
22867
|
-
const currentPreview = previousDeployments.find((release) => release.kind === "preview" && matchesSource(release, commit));
|
|
22868
|
-
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" }) });
|
|
22869
|
-
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 });
|
|
22870
|
-
if (overview.preview.commitSha !== commit) {
|
|
22871
|
-
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.");
|
|
22872
|
-
const refreshed = await call2(prefix + "/developer/git-credential", { method: "POST" });
|
|
22873
|
-
if (refreshed.repository.toLowerCase() !== status.repository.toLowerCase()) throw new Error("Repository changed while building.");
|
|
22874
|
-
git2(["fetch", "--", destination, "refs/heads/" + status.branch], root, gitEnvironment(status.repository, refreshed.token));
|
|
22875
|
-
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" });
|
|
22876
23210
|
}
|
|
22877
|
-
return
|
|
23211
|
+
return { template: "react-vite-hono", created: pending, unchanged: Object.keys(files).length - pending.length, installed: false, deployed: false };
|
|
22878
23212
|
}
|
|
22879
|
-
|
|
22880
|
-
|
|
22881
|
-
|
|
22882
|
-
|
|
22883
|
-
|
|
22884
|
-
if (
|
|
22885
|
-
const
|
|
22886
|
-
|
|
22887
|
-
|
|
22888
|
-
|
|
22889
|
-
|
|
22890
|
-
|
|
22891
|
-
|
|
22892
|
-
|
|
22893
|
-
|
|
22894
|
-
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.");
|
|
22895
|
-
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;
|
|
22896
23228
|
}
|
|
22897
|
-
|
|
22898
|
-
|
|
22899
|
-
|
|
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 };
|
|
22900
23250
|
}
|
|
22901
23251
|
|
|
22902
23252
|
// src/index.ts
|
|
22903
23253
|
import { readFileSync as readFileSync24, existsSync as existsSync15, rmSync as rmSync3 } from "node:fs";
|
|
22904
23254
|
import { homedir as homedir2 } from "node:os";
|
|
22905
23255
|
import { join as join17 } from "node:path";
|
|
22906
|
-
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
22907
|
-
import { createServer as createServer3 } from "node:http";
|
|
22908
23256
|
import { spawn as spawn5 } from "node:child_process";
|
|
22909
23257
|
var configPath = join17(homedir2(), ".backify.json");
|
|
22910
23258
|
var userConfig = existsSync15(configPath) ? JSON.parse(readFileSync24(configPath, "utf8")) : {};
|
|
@@ -22912,73 +23260,42 @@ var [command2, ...args] = process.argv.slice(2);
|
|
|
22912
23260
|
async function call(path, init) {
|
|
22913
23261
|
let appId = option("--app");
|
|
22914
23262
|
if (!appId && existsSync15(join17(process.cwd(), "backify.json"))) appId = projectApplication();
|
|
22915
|
-
const session = appId ? userConfig.sessions?.[
|
|
22916
|
-
|
|
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);
|
|
22917
23266
|
}
|
|
22918
23267
|
var option = (name2) => {
|
|
22919
23268
|
const index = args.indexOf(name2);
|
|
22920
23269
|
return index >= 0 ? args[index + 1] : void 0;
|
|
22921
23270
|
};
|
|
22922
23271
|
function openBrowser(url) {
|
|
22923
|
-
const
|
|
22924
|
-
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."));
|
|
22925
23275
|
child.unref();
|
|
22926
23276
|
}
|
|
22927
|
-
|
|
22928
|
-
|
|
22929
|
-
|
|
22930
|
-
|
|
22931
|
-
|
|
22932
|
-
|
|
22933
|
-
|
|
22934
|
-
|
|
22935
|
-
|
|
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;
|
|
22936
23293
|
}
|
|
22937
|
-
|
|
22938
|
-
|
|
22939
|
-
|
|
22940
|
-
|
|
22941
|
-
|
|
22942
|
-
const value = JSON.parse(raw);
|
|
22943
|
-
if (value.state !== state || typeof value.token !== "string" || typeof value.organizationId !== "string") throw new Error("Invalid callback");
|
|
22944
|
-
res.writeHead(200, { "access-control-allow-origin": "*", "content-type": "text/plain" });
|
|
22945
|
-
res.end("Backify CLI connected. You can close this tab.");
|
|
22946
|
-
userConfig.token = value.token;
|
|
22947
|
-
userConfig.organizationId = value.organizationId;
|
|
22948
|
-
saveUserConfig(configPath, userConfig);
|
|
22949
|
-
server.close();
|
|
22950
|
-
resolveLogin();
|
|
22951
|
-
} catch {
|
|
22952
|
-
res.writeHead(400, { "access-control-allow-origin": "*" });
|
|
22953
|
-
res.end("Invalid or expired Backify CLI login.");
|
|
22954
|
-
}
|
|
22955
|
-
});
|
|
22956
|
-
});
|
|
22957
|
-
let resolveLogin = () => {
|
|
22958
|
-
};
|
|
22959
|
-
const completed = new Promise((resolve24) => resolveLogin = resolve24);
|
|
22960
|
-
server.listen(0, "127.0.0.1", () => {
|
|
22961
|
-
const address = server.address();
|
|
22962
|
-
if (!address || typeof address === "string") throw new Error("Could not start local login callback");
|
|
22963
|
-
const callback = `http://127.0.0.1:${address.port}/callback`;
|
|
22964
|
-
const portal = new URL(process.env.BACKIFY_PORTAL_URL ?? userConfig.portalUrl ?? "https://backify.dev");
|
|
22965
|
-
portal.searchParams.set("cli_callback", callback);
|
|
22966
|
-
portal.searchParams.set("cli_state", state);
|
|
22967
|
-
console.log(`Opening ${portal.origin} to sign in to Backify\u2026`);
|
|
22968
|
-
openBrowser(portal.toString());
|
|
22969
|
-
});
|
|
22970
|
-
const timeout = setTimeout(() => {
|
|
22971
|
-
server.close();
|
|
22972
|
-
rejectLogin(new Error("Backify login timed out. Run backify login to try again."));
|
|
22973
|
-
}, 5 * 6e4);
|
|
22974
|
-
let rejectLogin = () => {
|
|
22975
|
-
};
|
|
22976
|
-
const failed = new Promise((_, reject) => rejectLogin = reject);
|
|
22977
|
-
try {
|
|
22978
|
-
await Promise.race([completed, failed]);
|
|
22979
|
-
} finally {
|
|
22980
|
-
clearTimeout(timeout);
|
|
22981
|
-
}
|
|
23294
|
+
saveUserConfig(configPath, userConfig);
|
|
23295
|
+
} });
|
|
23296
|
+
}
|
|
23297
|
+
function applicationClient(session, appId) {
|
|
23298
|
+
return reconnectingApiClient(session, () => browserLogin(appId, session.organizationId, session.apiUrl));
|
|
22982
23299
|
}
|
|
22983
23300
|
async function main() {
|
|
22984
23301
|
switch (command2) {
|
|
@@ -23126,11 +23443,11 @@ async function main() {
|
|
|
23126
23443
|
break;
|
|
23127
23444
|
}
|
|
23128
23445
|
case "login":
|
|
23129
|
-
if (args[0]) {
|
|
23446
|
+
if (args[0] && !args[0].startsWith("--")) {
|
|
23130
23447
|
userConfig.token = args[0];
|
|
23131
23448
|
userConfig.organizationId = args[1] ?? userConfig.organizationId;
|
|
23132
23449
|
saveUserConfig(configPath, userConfig);
|
|
23133
|
-
} else await browserLogin();
|
|
23450
|
+
} else await browserLogin(option("--app") ?? (existsSync15(join17(process.cwd(), "backify.json")) ? projectApplication() : void 0), option("--organization"));
|
|
23134
23451
|
console.log("Logged in to Backify");
|
|
23135
23452
|
break;
|
|
23136
23453
|
case "logout":
|
|
@@ -23139,23 +23456,34 @@ async function main() {
|
|
|
23139
23456
|
break;
|
|
23140
23457
|
case "connect": {
|
|
23141
23458
|
const appId = option("--app") ?? projectApplication(), key = "app_" + applicationGuid(appId).replaceAll("-", "");
|
|
23142
|
-
const code = option("--pair"), organizationId = option("--organization") ?? userConfig.organizationId;
|
|
23459
|
+
const code = option("--pair"), organizationId = option("--organization") ?? userConfig.sessions?.[key]?.organizationId ?? userConfig.organizationId;
|
|
23143
23460
|
const address = apiAddress(option("--api") ?? process.env.BACKIFY_API_URL ?? userConfig.apiUrl ?? "https://api.backify.dev/api");
|
|
23144
23461
|
if (code) {
|
|
23145
23462
|
if (!organizationId) throw new Error("The setup prompt must supply the organization.");
|
|
23146
23463
|
const redeem = apiClient({ token: "", organizationId, apiUrl: address });
|
|
23147
|
-
const paired = await redeem("/apps/" + applicationGuid(appId) + "/developer/pair", { method: "POST", body: JSON.stringify({ code }) });
|
|
23464
|
+
const paired = await redeem("/apps/" + applicationGuid(appId) + "/developer/pair", { method: "POST", body: JSON.stringify({ code, device: cliDevice() }) });
|
|
23148
23465
|
userConfig.sessions ??= {};
|
|
23149
23466
|
userConfig.sessions[key] = { ...paired, apiUrl: address };
|
|
23150
23467
|
saveUserConfig(configPath, userConfig);
|
|
23151
|
-
} else if (!userConfig.sessions?.[key] && !userConfig.token) {
|
|
23152
|
-
await browserLogin();
|
|
23468
|
+
} else if (args.includes("--reauth") || !userConfig.sessions?.[key] && !userConfig.token) {
|
|
23469
|
+
await browserLogin(appId, organizationId, address);
|
|
23470
|
+
}
|
|
23471
|
+
let session = userConfig.sessions?.[key] ?? { token: userConfig.token ?? "", organizationId: organizationId ?? userConfig.organizationId ?? "", apiUrl: address };
|
|
23472
|
+
if (!session.token.startsWith("bfcli_")) {
|
|
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
|
+
}
|
|
23479
|
+
userConfig.sessions ??= {};
|
|
23480
|
+
userConfig.sessions[key] = session;
|
|
23481
|
+
saveUserConfig(configPath, userConfig);
|
|
23153
23482
|
}
|
|
23154
|
-
const
|
|
23155
|
-
const client = apiClient(session);
|
|
23483
|
+
const client = applicationClient(session, appId);
|
|
23156
23484
|
await client("/apps/" + applicationGuid(appId) + "/developer");
|
|
23157
23485
|
userConfig.sessions ??= {};
|
|
23158
|
-
userConfig.sessions[key]
|
|
23486
|
+
userConfig.sessions[key] ??= session;
|
|
23159
23487
|
saveUserConfig(configPath, userConfig);
|
|
23160
23488
|
if (args.includes("--clone")) await cloneProject(client, appId);
|
|
23161
23489
|
else if (existsSync15(join17(process.cwd(), ".git"))) {
|
|
@@ -23189,7 +23517,7 @@ async function main() {
|
|
|
23189
23517
|
const latest = deployments[0];
|
|
23190
23518
|
if (latest?.status === "failed") console.log(JSON.stringify(await call(prefix + "/deployments/" + latest.id), null, 2));
|
|
23191
23519
|
try {
|
|
23192
|
-
const commit =
|
|
23520
|
+
const commit = git(["rev-parse", "HEAD"]);
|
|
23193
23521
|
console.log(JSON.stringify(await call(prefix + "/developer/build?commitSha=" + commit), null, 2));
|
|
23194
23522
|
} catch {
|
|
23195
23523
|
console.log("Build status is unavailable for the current local commit.");
|
|
@@ -23269,7 +23597,7 @@ async function main() {
|
|
|
23269
23597
|
}
|
|
23270
23598
|
main().catch((error) => {
|
|
23271
23599
|
const message = error instanceof Error ? error.message : String(error);
|
|
23272
|
-
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")) : "") } : {} }));
|
|
23273
23601
|
else console.error(message);
|
|
23274
23602
|
process.exitCode = 1;
|
|
23275
23603
|
});
|