tines 0.0.236 → 0.0.237
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 +6 -0
- package/dist/index.js +516 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,12 @@ tines workflows preview review.json --choices choices.json --plan-out review.pla
|
|
|
73
73
|
tines workflows install review.json --plan review.plan.json --confirm sha256:<plan-digest>
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
`preview` and `install` also accept a canonical public snapshot URL. Same-instance URLs keep hosted
|
|
77
|
+
withdrawal checks; foreign URLs are fetched directly by the CLI without forwarding the destination
|
|
78
|
+
API key, cookies, proxy authorization, or referrer. A foreign-source install re-downloads the URL and
|
|
79
|
+
requires the exact reviewed bytes. See the
|
|
80
|
+
[workflow package guide](https://github.com/tbuckley/tines/blob/main/docs/workflow-packages.md).
|
|
81
|
+
|
|
76
82
|
Workflow names are accepted only when unique; use the ID when duplicate names exist. Export
|
|
77
83
|
selection is explicit: `--project`, repeatable `--schedule`, repeatable
|
|
78
84
|
`--tier '<state-id>=balanced'`, `--project-routing`, and `--inputs declarations.json` add
|
package/dist/index.js
CHANGED
|
@@ -4225,6 +4225,48 @@ function createApiClient(options) {
|
|
|
4225
4225
|
getWorkflowPackageReceipt: (planId) => get(
|
|
4226
4226
|
`/api/v1/library/installs/${encodeURIComponent(planId)}`
|
|
4227
4227
|
),
|
|
4228
|
+
validatePublication: (body) => request(
|
|
4229
|
+
"POST",
|
|
4230
|
+
"/api/v1/publications/validate",
|
|
4231
|
+
body
|
|
4232
|
+
),
|
|
4233
|
+
preparePublication: (body) => request(
|
|
4234
|
+
"POST",
|
|
4235
|
+
"/api/v1/publications/prepare",
|
|
4236
|
+
body
|
|
4237
|
+
),
|
|
4238
|
+
publishPublication: (candidateId, body) => request(
|
|
4239
|
+
"POST",
|
|
4240
|
+
`/api/v1/publications/${encodeURIComponent(candidateId)}/publish`,
|
|
4241
|
+
body
|
|
4242
|
+
),
|
|
4243
|
+
getPublicationResult: (candidateId) => get(
|
|
4244
|
+
`/api/v1/publications/${encodeURIComponent(candidateId)}/result`
|
|
4245
|
+
),
|
|
4246
|
+
listPublications: (workflow2) => get(
|
|
4247
|
+
`/api/v1/publications${query({ workflow: workflow2 })}`
|
|
4248
|
+
),
|
|
4249
|
+
withdrawPublication: (snapshotId) => request(
|
|
4250
|
+
"POST",
|
|
4251
|
+
`/api/v1/publications/${encodeURIComponent(snapshotId)}/withdraw`
|
|
4252
|
+
),
|
|
4253
|
+
restorePublication: (snapshotId) => request(
|
|
4254
|
+
"POST",
|
|
4255
|
+
`/api/v1/publications/${encodeURIComponent(snapshotId)}/restore`
|
|
4256
|
+
),
|
|
4257
|
+
getPublicSnapshot: (snapshotId) => get(
|
|
4258
|
+
`/api/v1/publications/public/${encodeURIComponent(snapshotId)}`
|
|
4259
|
+
),
|
|
4260
|
+
getPublicSnapshotStatus: (snapshotId) => get(
|
|
4261
|
+
`/api/v1/publications/public/${encodeURIComponent(snapshotId)}/status`
|
|
4262
|
+
),
|
|
4263
|
+
prepareHostedWorkflowPackage: (snapshotId, choices = {}) => request(
|
|
4264
|
+
"POST",
|
|
4265
|
+
`/api/v1/publications/public/${encodeURIComponent(snapshotId)}/prepare-install`,
|
|
4266
|
+
{ choices }
|
|
4267
|
+
),
|
|
4268
|
+
downloadPublicSnapshot: (snapshotId) => raw("GET", `/api/v1/publications/public/${encodeURIComponent(snapshotId)}/download`),
|
|
4269
|
+
downloadPublicationReuseNotice: (snapshotId) => raw("GET", `/api/v1/publications/public/${encodeURIComponent(snapshotId)}/reuse.txt`),
|
|
4228
4270
|
validateLibrary: (body) => request(
|
|
4229
4271
|
"POST",
|
|
4230
4272
|
"/api/v1/library/validate",
|
|
@@ -5494,6 +5536,27 @@ function parseStrictLibraryJson(source) {
|
|
|
5494
5536
|
// ../shared/src/library/choices.ts
|
|
5495
5537
|
var PACKAGE_CHOICES_MAX_BYTES = 512 * 1024;
|
|
5496
5538
|
|
|
5539
|
+
// ../shared/src/publications.ts
|
|
5540
|
+
var PUBLIC_WORKFLOW_MAX_BYTES = 1048576;
|
|
5541
|
+
var PUBLICATION_CANDIDATE_TTL_MS = 15 * 60 * 1e3;
|
|
5542
|
+
var SNAPSHOT_ID = /^[A-Za-z0-9_-]{20,100}$/;
|
|
5543
|
+
function parsePublicSnapshotReference(value, configuredOrigin) {
|
|
5544
|
+
if (SNAPSHOT_ID.test(value)) return value;
|
|
5545
|
+
let url;
|
|
5546
|
+
try {
|
|
5547
|
+
url = new URL(value);
|
|
5548
|
+
} catch {
|
|
5549
|
+
throw new Error("Expected a publication ID or canonical public URL");
|
|
5550
|
+
}
|
|
5551
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
|
|
5552
|
+
throw new Error("Invalid public publication URL");
|
|
5553
|
+
if (configuredOrigin && url.origin !== new URL(configuredOrigin).origin)
|
|
5554
|
+
throw new Error("external_source_requires_download");
|
|
5555
|
+
const match = /^\/p\/([A-Za-z0-9_-]{20,100})(?:\/download)?$/.exec(url.pathname);
|
|
5556
|
+
if (!match || url.search) throw new Error("Invalid public publication URL");
|
|
5557
|
+
return match[1];
|
|
5558
|
+
}
|
|
5559
|
+
|
|
5497
5560
|
// ../shared/src/usage.ts
|
|
5498
5561
|
function usageCostLabel(cost, finalized = 1) {
|
|
5499
5562
|
if (finalized === 0) return "No runs";
|
|
@@ -12497,6 +12560,7 @@ function register10(program3) {
|
|
|
12497
12560
|
|
|
12498
12561
|
// src/commands/workflows.ts
|
|
12499
12562
|
import { readFileSync as readFileSync12 } from "node:fs";
|
|
12563
|
+
import { createInterface as createInterface5 } from "node:readline/promises";
|
|
12500
12564
|
|
|
12501
12565
|
// src/workflow-packages.ts
|
|
12502
12566
|
import { createInterface as createInterface4 } from "node:readline/promises";
|
|
@@ -12526,12 +12590,13 @@ function readStrictObject(path2, label) {
|
|
|
12526
12590
|
function normalizeApiBase(value) {
|
|
12527
12591
|
return new URL(value).toString().replace(/\/$/, "");
|
|
12528
12592
|
}
|
|
12529
|
-
function saveWorkflowPackagePlan(path2, apiBase, plan) {
|
|
12593
|
+
function saveWorkflowPackagePlan(path2, apiBase, plan, remoteSource) {
|
|
12530
12594
|
const saved = {
|
|
12531
12595
|
format: "tines.workflow-install-plan",
|
|
12532
12596
|
version: 1,
|
|
12533
12597
|
api_base: normalizeApiBase(apiBase),
|
|
12534
12598
|
document_digest: plan.document_digest,
|
|
12599
|
+
...remoteSource ? { remote_source: remoteSource } : {},
|
|
12535
12600
|
plan
|
|
12536
12601
|
};
|
|
12537
12602
|
writeJsonFile(path2, saved, { secret: true });
|
|
@@ -12539,7 +12604,7 @@ function saveWorkflowPackagePlan(path2, apiBase, plan) {
|
|
|
12539
12604
|
}
|
|
12540
12605
|
function readWorkflowPackagePlan(path2) {
|
|
12541
12606
|
const saved = readStrictObject(path2, "plan file");
|
|
12542
|
-
const outerKeys = ["format", "version", "api_base", "document_digest", "plan"];
|
|
12607
|
+
const outerKeys = ["format", "version", "api_base", "document_digest", "remote_source", "plan"];
|
|
12543
12608
|
const planKeys = [
|
|
12544
12609
|
"operations",
|
|
12545
12610
|
"document",
|
|
@@ -12553,9 +12618,17 @@ function readWorkflowPackagePlan(path2) {
|
|
|
12553
12618
|
"actor_key",
|
|
12554
12619
|
"compiler_version",
|
|
12555
12620
|
"plan_token",
|
|
12556
|
-
"budget"
|
|
12621
|
+
"budget",
|
|
12622
|
+
"source"
|
|
12557
12623
|
];
|
|
12558
|
-
if (Object.keys(saved).length
|
|
12624
|
+
if (![5, 6].includes(Object.keys(saved).length) || !Object.keys(saved).every((key) => outerKeys.includes(key)) || saved.format !== "tines.workflow-install-plan" || saved.version !== 1 || typeof saved.api_base !== "string" || typeof saved.document_digest !== "string" || !saved.plan || typeof saved.plan !== "object" || typeof saved.plan.plan_token !== "string" || typeof saved.plan.plan_digest !== "string" || ![planKeys.length - 1, planKeys.length].includes(
|
|
12625
|
+
Object.keys(saved.plan).length
|
|
12626
|
+
) || !Object.keys(saved.plan).every((key) => planKeys.includes(key))) {
|
|
12627
|
+
throw new Error("invalid workflow package plan file");
|
|
12628
|
+
}
|
|
12629
|
+
if (saved.remote_source !== void 0 && (!saved.remote_source || typeof saved.remote_source !== "object" || Object.keys(saved.remote_source).length !== 2 || typeof saved.remote_source.url !== "string" || !/^sha256:[0-9a-f]{64}$/.test(
|
|
12630
|
+
String(saved.remote_source.bytes_sha256)
|
|
12631
|
+
))) {
|
|
12559
12632
|
throw new Error("invalid workflow package plan file");
|
|
12560
12633
|
}
|
|
12561
12634
|
const result = saved;
|
|
@@ -12677,7 +12750,8 @@ function assertPlanBinding(plan) {
|
|
|
12677
12750
|
actor_key: plan.actor_key,
|
|
12678
12751
|
compiler_version: plan.compiler_version,
|
|
12679
12752
|
allocation: plan.allocation,
|
|
12680
|
-
budget: plan.budget
|
|
12753
|
+
budget: plan.budget,
|
|
12754
|
+
...plan.source ? { source: plan.source } : {}
|
|
12681
12755
|
};
|
|
12682
12756
|
for (const [field, value] of Object.entries(mirrors)) {
|
|
12683
12757
|
const signedField = field === "id" ? "id" : field;
|
|
@@ -12794,10 +12868,16 @@ function formatValidation(result) {
|
|
|
12794
12868
|
}
|
|
12795
12869
|
async function localDocument(path2) {
|
|
12796
12870
|
const raw = readPackageSource(path2);
|
|
12871
|
+
return packageDocument(raw);
|
|
12872
|
+
}
|
|
12873
|
+
async function packageDocument(raw) {
|
|
12797
12874
|
const document = await parseLibraryV3Document(raw);
|
|
12798
12875
|
if (document.profile !== "workflow") throw new Error("expected a workflow-profile package");
|
|
12799
12876
|
return { raw, document };
|
|
12800
12877
|
}
|
|
12878
|
+
function workflowPackageBytesSha256(raw) {
|
|
12879
|
+
return `sha256:${createHash2("sha256").update(raw).digest("hex")}`;
|
|
12880
|
+
}
|
|
12801
12881
|
function canonicalWorkflowPackage(document) {
|
|
12802
12882
|
return canonicalizeLibraryValue(document);
|
|
12803
12883
|
}
|
|
@@ -12841,7 +12921,218 @@ async function recoverOrInstall(api, raw, plan) {
|
|
|
12841
12921
|
}
|
|
12842
12922
|
}
|
|
12843
12923
|
|
|
12924
|
+
// src/publication-fetch.ts
|
|
12925
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
12926
|
+
import { request as httpRequest } from "node:http";
|
|
12927
|
+
import { request as httpsRequest } from "node:https";
|
|
12928
|
+
import { isIP } from "node:net";
|
|
12929
|
+
var SNAPSHOT_PATH = /^\/p\/[A-Za-z0-9_-]{20,100}(?:\/download)?$/;
|
|
12930
|
+
var MAX_REDIRECTS = 3;
|
|
12931
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
12932
|
+
function mappedIpv4(address) {
|
|
12933
|
+
const match = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i.exec(address);
|
|
12934
|
+
if (match) return match[1];
|
|
12935
|
+
const hexadecimal = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(address);
|
|
12936
|
+
if (!hexadecimal) return void 0;
|
|
12937
|
+
const high = Number.parseInt(hexadecimal[1], 16);
|
|
12938
|
+
const low = Number.parseInt(hexadecimal[2], 16);
|
|
12939
|
+
return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
|
|
12940
|
+
}
|
|
12941
|
+
function isLoopback(address) {
|
|
12942
|
+
const mapped = mappedIpv4(address);
|
|
12943
|
+
if (mapped) return isLoopback(mapped);
|
|
12944
|
+
return address === "::1" || /^127\./.test(address);
|
|
12945
|
+
}
|
|
12946
|
+
function ipv6Integer(address) {
|
|
12947
|
+
let value = address.toLowerCase();
|
|
12948
|
+
const dotted = /(?:^|:)(\d{1,3}(?:\.\d{1,3}){3})$/.exec(value);
|
|
12949
|
+
if (dotted) {
|
|
12950
|
+
const octets = dotted[1].split(".").map(Number);
|
|
12951
|
+
if (octets.some((octet) => octet > 255)) return void 0;
|
|
12952
|
+
value = `${value.slice(0, dotted.index)}:${(octets[0] << 8 | octets[1]).toString(16)}:${(octets[2] << 8 | octets[3]).toString(16)}`;
|
|
12953
|
+
}
|
|
12954
|
+
const halves = value.split("::");
|
|
12955
|
+
if (halves.length > 2) return void 0;
|
|
12956
|
+
const left = halves[0] ? halves[0].split(":") : [];
|
|
12957
|
+
const right = halves[1] ? halves[1].split(":") : [];
|
|
12958
|
+
const missing = 8 - left.length - right.length;
|
|
12959
|
+
if (halves.length === 1 && missing !== 0 || missing < 0) return void 0;
|
|
12960
|
+
const groups = [...left, ...Array(missing).fill("0"), ...right];
|
|
12961
|
+
if (groups.length !== 8 || groups.some((group) => !/^[0-9a-f]{1,4}$/.test(group)))
|
|
12962
|
+
return void 0;
|
|
12963
|
+
return groups.reduce((result, group) => result << 16n | BigInt(`0x${group}`), 0n);
|
|
12964
|
+
}
|
|
12965
|
+
function inIpv6Range(value, base, prefix) {
|
|
12966
|
+
const baseValue = ipv6Integer(base);
|
|
12967
|
+
if (baseValue === void 0) return false;
|
|
12968
|
+
const shift = BigInt(128 - prefix);
|
|
12969
|
+
return value >> shift === baseValue >> shift;
|
|
12970
|
+
}
|
|
12971
|
+
function isPublicAddress(address) {
|
|
12972
|
+
const mapped = mappedIpv4(address);
|
|
12973
|
+
if (mapped) return isPublicAddress(mapped);
|
|
12974
|
+
if (isIP(address) === 4) {
|
|
12975
|
+
const [a, b] = address.split(".").map(Number);
|
|
12976
|
+
return !(a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 0 || a === 192 && b === 2 || a === 192 && b === 88 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19) || a === 198 && b === 51 || a === 203 && b === 0 && Number(address.split(".")[2]) === 113);
|
|
12977
|
+
}
|
|
12978
|
+
if (isIP(address) === 6) {
|
|
12979
|
+
const value = ipv6Integer(address);
|
|
12980
|
+
if (value === void 0 || !inIpv6Range(value, "2000::", 3)) return false;
|
|
12981
|
+
return ![
|
|
12982
|
+
["2001::", 23],
|
|
12983
|
+
// IETF protocol assignments, including Teredo and benchmarking
|
|
12984
|
+
["2001:db8::", 32],
|
|
12985
|
+
// documentation
|
|
12986
|
+
["2002::", 16],
|
|
12987
|
+
// 6to4 (may embed a private IPv4 destination)
|
|
12988
|
+
["2620:4f:8000::", 48],
|
|
12989
|
+
// documentation
|
|
12990
|
+
["3fff::", 20]
|
|
12991
|
+
// documentation
|
|
12992
|
+
].some(([base, prefix]) => inIpv6Range(value, base, prefix));
|
|
12993
|
+
}
|
|
12994
|
+
return false;
|
|
12995
|
+
}
|
|
12996
|
+
function validateHop(url, initial) {
|
|
12997
|
+
if (!["http:", "https:"].includes(url.protocol))
|
|
12998
|
+
throw new Error("publication URL must use HTTPS");
|
|
12999
|
+
if (url.username || url.password) throw new Error("publication URL must not contain credentials");
|
|
13000
|
+
if (url.hash) throw new Error("publication URL must not contain a fragment");
|
|
13001
|
+
if (url.search) throw new Error("publication URL must not contain query parameters");
|
|
13002
|
+
if (initial && !SNAPSHOT_PATH.test(url.pathname))
|
|
13003
|
+
throw new Error("expected a canonical public snapshot or download URL");
|
|
13004
|
+
}
|
|
13005
|
+
async function pinnedAddress(url, lookup) {
|
|
13006
|
+
const hostname4 = url.hostname.replace(/^\[|\]$/g, "");
|
|
13007
|
+
const literalFamily = isIP(hostname4);
|
|
13008
|
+
const addresses = literalFamily ? [{ address: hostname4, family: literalFamily }] : await lookup(hostname4, { all: true, verbatim: true });
|
|
13009
|
+
if (!addresses.length) throw new Error(`publication host ${url.hostname} did not resolve`);
|
|
13010
|
+
const loopbackDevelopment = url.protocol === "http:" && (url.hostname === "localhost" || addresses.every(({ address }) => isLoopback(address)));
|
|
13011
|
+
if (url.protocol === "http:" && !loopbackDevelopment)
|
|
13012
|
+
throw new Error(
|
|
13013
|
+
"publication URL must use HTTPS (HTTP is allowed only for loopback development)"
|
|
13014
|
+
);
|
|
13015
|
+
if (!loopbackDevelopment && addresses.some(({ address }) => !isPublicAddress(address)))
|
|
13016
|
+
throw new Error("publication URL resolves to a private or reserved address");
|
|
13017
|
+
return addresses[0];
|
|
13018
|
+
}
|
|
13019
|
+
async function readHop(url, options) {
|
|
13020
|
+
const pinned = await pinnedAddress(url, options.lookup);
|
|
13021
|
+
return new Promise((resolve2, reject) => {
|
|
13022
|
+
const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(
|
|
13023
|
+
url,
|
|
13024
|
+
{
|
|
13025
|
+
method: "GET",
|
|
13026
|
+
headers: { accept: "application/json" },
|
|
13027
|
+
lookup: (_hostname, _opts, callback) => callback(null, pinned.address, pinned.family)
|
|
13028
|
+
},
|
|
13029
|
+
(response) => {
|
|
13030
|
+
const status = response.statusCode ?? 0;
|
|
13031
|
+
if (status >= 300 && status < 400) {
|
|
13032
|
+
response.resume();
|
|
13033
|
+
const location = response.headers.location;
|
|
13034
|
+
if (!location) return reject(new Error("publication redirect has no destination"));
|
|
13035
|
+
try {
|
|
13036
|
+
return resolve2({ redirect: new URL(location, url) });
|
|
13037
|
+
} catch {
|
|
13038
|
+
return reject(new Error("publication redirect has an invalid destination"));
|
|
13039
|
+
}
|
|
13040
|
+
}
|
|
13041
|
+
if (status !== 200) {
|
|
13042
|
+
response.resume();
|
|
13043
|
+
return reject(new Error(`publication download returned HTTP ${status}`));
|
|
13044
|
+
}
|
|
13045
|
+
const contentType = String(response.headers["content-type"] ?? "").split(";", 1)[0].trim().toLowerCase();
|
|
13046
|
+
if (contentType !== "application/json") {
|
|
13047
|
+
response.resume();
|
|
13048
|
+
return reject(new Error("publication download must be application/json"));
|
|
13049
|
+
}
|
|
13050
|
+
const encoding = String(response.headers["content-encoding"] ?? "identity").toLowerCase();
|
|
13051
|
+
if (encoding !== "identity") {
|
|
13052
|
+
response.resume();
|
|
13053
|
+
return reject(new Error("publication download uses unsupported content encoding"));
|
|
13054
|
+
}
|
|
13055
|
+
let size = 0;
|
|
13056
|
+
const chunks = [];
|
|
13057
|
+
response.on("data", (chunk) => {
|
|
13058
|
+
size += chunk.length;
|
|
13059
|
+
if (size > options.maxBytes)
|
|
13060
|
+
response.destroy(new Error("publication download is too large"));
|
|
13061
|
+
else chunks.push(chunk);
|
|
13062
|
+
});
|
|
13063
|
+
response.on("end", () => {
|
|
13064
|
+
try {
|
|
13065
|
+
resolve2({
|
|
13066
|
+
body: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
|
13067
|
+
Buffer.concat(chunks)
|
|
13068
|
+
)
|
|
13069
|
+
});
|
|
13070
|
+
} catch {
|
|
13071
|
+
reject(new Error("publication download is not valid UTF-8"));
|
|
13072
|
+
}
|
|
13073
|
+
});
|
|
13074
|
+
}
|
|
13075
|
+
);
|
|
13076
|
+
const deadline = setTimeout(
|
|
13077
|
+
() => request.destroy(new Error("publication download timed out")),
|
|
13078
|
+
options.timeoutMs
|
|
13079
|
+
);
|
|
13080
|
+
request.on("close", () => clearTimeout(deadline));
|
|
13081
|
+
request.on("error", reject);
|
|
13082
|
+
request.end();
|
|
13083
|
+
});
|
|
13084
|
+
}
|
|
13085
|
+
async function fetchPublicWorkflowPackage(value, options = {}) {
|
|
13086
|
+
let url;
|
|
13087
|
+
try {
|
|
13088
|
+
url = new URL(value);
|
|
13089
|
+
} catch {
|
|
13090
|
+
throw new Error("expected a canonical public snapshot or download URL");
|
|
13091
|
+
}
|
|
13092
|
+
validateHop(url, true);
|
|
13093
|
+
if (!url.pathname.endsWith("/download")) url.pathname += "/download";
|
|
13094
|
+
const sourceUrl = url.toString();
|
|
13095
|
+
for (let redirects = 0; ; redirects += 1) {
|
|
13096
|
+
validateHop(url, false);
|
|
13097
|
+
const result = await readHop(url, {
|
|
13098
|
+
lookup: options.lookup ?? dnsLookup,
|
|
13099
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
13100
|
+
maxBytes: options.maxBytes ?? PUBLIC_WORKFLOW_MAX_BYTES
|
|
13101
|
+
});
|
|
13102
|
+
if (result.body !== void 0) return { raw: result.body, sourceUrl };
|
|
13103
|
+
if (!result.redirect || redirects >= MAX_REDIRECTS)
|
|
13104
|
+
throw new Error("publication download exceeded three redirects");
|
|
13105
|
+
if (url.protocol === "https:" && result.redirect.protocol !== "https:")
|
|
13106
|
+
throw new Error("publication redirect must not downgrade HTTPS");
|
|
13107
|
+
url = result.redirect;
|
|
13108
|
+
}
|
|
13109
|
+
}
|
|
13110
|
+
|
|
12844
13111
|
// src/commands/workflows.ts
|
|
13112
|
+
function savePublicationProof(path2, apiBase, proof) {
|
|
13113
|
+
const saved = {
|
|
13114
|
+
format: "tines.workflow-publication-proof",
|
|
13115
|
+
version: 1,
|
|
13116
|
+
api_base: normalizeUrl(apiBase),
|
|
13117
|
+
proof
|
|
13118
|
+
};
|
|
13119
|
+
writeJsonFile(path2, saved, { secret: true });
|
|
13120
|
+
return saved;
|
|
13121
|
+
}
|
|
13122
|
+
function readPublicationProof(path2) {
|
|
13123
|
+
const saved = readStrictObject(path2, "publication proof");
|
|
13124
|
+
if (saved.format !== "tines.workflow-publication-proof" || saved.version !== 1 || typeof saved.api_base !== "string" || !saved.proof || typeof saved.proof.candidate_id !== "string" || typeof saved.proof.review_digest !== "string")
|
|
13125
|
+
die("invalid workflow publication proof file");
|
|
13126
|
+
return saved;
|
|
13127
|
+
}
|
|
13128
|
+
async function confirmPublicationAction(question) {
|
|
13129
|
+
const rl = createInterface5({ input: process.stdin, output: process.stderr });
|
|
13130
|
+
try {
|
|
13131
|
+
return /^(?:y|yes)$/i.test((await rl.question(`${question} [y/N] `)).trim());
|
|
13132
|
+
} finally {
|
|
13133
|
+
rl.close();
|
|
13134
|
+
}
|
|
13135
|
+
}
|
|
12845
13136
|
function readJsonBody(inline, file) {
|
|
12846
13137
|
if (inline !== void 0 && file !== void 0) {
|
|
12847
13138
|
die("pass the JSON inline or with --file, not both");
|
|
@@ -13152,7 +13443,150 @@ async function resolveExportState(api, ref) {
|
|
|
13152
13443
|
if (named.length > 1) die(`state name "${ref}" is ambiguous; use an id`);
|
|
13153
13444
|
die(`no state named "${ref}"; use a state id or <workflow>/<state>`);
|
|
13154
13445
|
}
|
|
13446
|
+
function publicationSource(value, apiBase) {
|
|
13447
|
+
if (!/^https?:\/\//i.test(value)) return { kind: "file" };
|
|
13448
|
+
try {
|
|
13449
|
+
return {
|
|
13450
|
+
kind: "hosted",
|
|
13451
|
+
snapshotId: parsePublicSnapshotReference(value, apiBase)
|
|
13452
|
+
};
|
|
13453
|
+
} catch (error) {
|
|
13454
|
+
if (error instanceof Error && error.message === "external_source_requires_download")
|
|
13455
|
+
return { kind: "remote" };
|
|
13456
|
+
throw error;
|
|
13457
|
+
}
|
|
13458
|
+
}
|
|
13155
13459
|
function registerPackageCommands(workflows) {
|
|
13460
|
+
withCommon(
|
|
13461
|
+
workflows.command("publication-validate <file>").description("Validate a workflow package for text-only public hosting")
|
|
13462
|
+
).action(async (path2, opts) => {
|
|
13463
|
+
const result = await client(opts).validatePublication({
|
|
13464
|
+
document_json: readPackageSource(path2)
|
|
13465
|
+
});
|
|
13466
|
+
if (opts.json) printJson(result);
|
|
13467
|
+
else {
|
|
13468
|
+
console.log(
|
|
13469
|
+
result.valid ? "valid public workflow snapshot" : "invalid public workflow snapshot"
|
|
13470
|
+
);
|
|
13471
|
+
for (const diagnostic of result.diagnostics)
|
|
13472
|
+
console.log(` ${diagnostic.path || "/"}: ${diagnostic.message}`);
|
|
13473
|
+
}
|
|
13474
|
+
if (!result.valid) process.exitCode = 1;
|
|
13475
|
+
});
|
|
13476
|
+
withCommon(
|
|
13477
|
+
workflows.command("publish [workflow]").description("Prepare or commit one exact immutable public workflow snapshot").option(
|
|
13478
|
+
"--from <file>",
|
|
13479
|
+
"prepare from an existing workflow package instead of an owned workflow"
|
|
13480
|
+
).option("--proof-out <file>", "save the complete prepared proof with mode 0600").option("--proof <file>", "commit a previously saved exact proof").option("--display-name <name>", "public attribution name (never an email)").option("--license <license>", "reuse license (currently MIT)", "MIT").option("--license-year <year>", "MIT copyright year", String((/* @__PURE__ */ new Date()).getFullYear())).option("--confirm <review-digest>", "exact digest of the reviewed proof").option("--sharing-rights", "confirm rights to every bundled declaration and file").option("--recover", "reconcile this same candidate after a lost response").option("--repo <local-id>", "confirm one bundled repository ID (repeatable)", collect2, [])
|
|
13481
|
+
).action(
|
|
13482
|
+
async (workflow2, opts) => {
|
|
13483
|
+
const apiBase = normalizeUrl(resolveUrl(opts));
|
|
13484
|
+
const api = client(opts);
|
|
13485
|
+
if (opts.proof) {
|
|
13486
|
+
if (workflow2 || opts.from || opts.proofOut || opts.displayName)
|
|
13487
|
+
die(
|
|
13488
|
+
"--proof commit mode cannot be combined with a workflow, --from, --proof-out, or --display-name"
|
|
13489
|
+
);
|
|
13490
|
+
const saved = readPublicationProof(opts.proof);
|
|
13491
|
+
if (normalizeUrl(saved.api_base) !== apiBase)
|
|
13492
|
+
die(`proof belongs to ${saved.api_base}, not ${apiBase}`);
|
|
13493
|
+
const proof2 = saved.proof;
|
|
13494
|
+
if (opts.confirm && opts.confirm !== proof2.review_digest)
|
|
13495
|
+
die(`confirmation digest does not match reviewed proof ${proof2.review_digest}`);
|
|
13496
|
+
if (!process.stdin.isTTY && (opts.confirm !== proof2.review_digest || !opts.sharingRights))
|
|
13497
|
+
die(`non-interactive publish requires --confirm ${proof2.review_digest} --sharing-rights`);
|
|
13498
|
+
if (process.stdin.isTTY && !opts.confirm) {
|
|
13499
|
+
process.stderr.write(`${JSON.stringify(proof2, null, 2)}
|
|
13500
|
+
`);
|
|
13501
|
+
if (!await confirmPublicationAction("Publish these exact immutable bytes?"))
|
|
13502
|
+
die("publication declined");
|
|
13503
|
+
if (!await confirmPublicationAction("Do you have sharing rights for every bundled item?"))
|
|
13504
|
+
die("publication declined");
|
|
13505
|
+
}
|
|
13506
|
+
let result;
|
|
13507
|
+
if (opts.recover) {
|
|
13508
|
+
try {
|
|
13509
|
+
result = await api.getPublicationResult(proof2.candidate_id);
|
|
13510
|
+
} catch (error) {
|
|
13511
|
+
if (!(error instanceof ApiError) || error.status !== 404) throw error;
|
|
13512
|
+
}
|
|
13513
|
+
}
|
|
13514
|
+
result ??= await api.publishPublication(proof2.candidate_id, {
|
|
13515
|
+
review_digest: proof2.review_digest,
|
|
13516
|
+
sharing_rights: true,
|
|
13517
|
+
exact_content: true,
|
|
13518
|
+
reviewed_repo_ids: opts.repo
|
|
13519
|
+
});
|
|
13520
|
+
if (opts.json) printJson(result);
|
|
13521
|
+
else console.log(`published ${result.receipt.public_url}`);
|
|
13522
|
+
return;
|
|
13523
|
+
}
|
|
13524
|
+
if (opts.recover) die("--recover requires --proof");
|
|
13525
|
+
if (!!workflow2 === !!opts.from) die("provide exactly one owned workflow or --from <file>");
|
|
13526
|
+
if (!opts.proofOut) die("preparing a publication requires --proof-out <file>");
|
|
13527
|
+
if (!opts.displayName) die("preparing a publication requires --display-name <name>");
|
|
13528
|
+
if (opts.license !== "MIT") die("--license currently supports only MIT");
|
|
13529
|
+
const year = Number(opts.licenseYear);
|
|
13530
|
+
if (!Number.isSafeInteger(year)) die("--license-year must be an integer");
|
|
13531
|
+
const source = opts.from ? { kind: "file", document_json: readPackageSource(opts.from) } : {
|
|
13532
|
+
kind: "owned_workflow",
|
|
13533
|
+
workflow_id: (await resolveWorkflow(api, workflow2)).id,
|
|
13534
|
+
options: { schedule_ids: [], tiers: [] }
|
|
13535
|
+
};
|
|
13536
|
+
const proof = await api.preparePublication({
|
|
13537
|
+
prepare_request_id: crypto.randomUUID(),
|
|
13538
|
+
source,
|
|
13539
|
+
metadata: { display_name: opts.displayName, license: "MIT", license_year: year }
|
|
13540
|
+
});
|
|
13541
|
+
savePublicationProof(opts.proofOut, apiBase, proof);
|
|
13542
|
+
if (opts.json) printJson(proof);
|
|
13543
|
+
else {
|
|
13544
|
+
console.log(JSON.stringify(proof, null, 2));
|
|
13545
|
+
console.error(`saved exact publication proof to ${opts.proofOut}`);
|
|
13546
|
+
}
|
|
13547
|
+
}
|
|
13548
|
+
);
|
|
13549
|
+
withCommon(
|
|
13550
|
+
workflows.command("publications").description("List your public workflow snapshots").option("--workflow <id>", "filter by owned source workflow ID")
|
|
13551
|
+
).action(async (opts) => {
|
|
13552
|
+
const result = await client(opts).listPublications(opts.workflow);
|
|
13553
|
+
if (opts.json) printJson(result);
|
|
13554
|
+
else if (!result.items.length) console.log("no public snapshots");
|
|
13555
|
+
else
|
|
13556
|
+
table([
|
|
13557
|
+
["NAME", "STATUS", "PUBLISHED", "SNAPSHOT"],
|
|
13558
|
+
...result.items.map((item) => [
|
|
13559
|
+
item.metadata.display_name,
|
|
13560
|
+
item.owner_state === "published" && item.host_state === "active" ? "hosted" : "unavailable",
|
|
13561
|
+
new Date(item.published_at).toISOString(),
|
|
13562
|
+
item.snapshot_id
|
|
13563
|
+
])
|
|
13564
|
+
]);
|
|
13565
|
+
});
|
|
13566
|
+
withCommon(
|
|
13567
|
+
workflows.command("unpublish <public-url-or-id>").description("Withdraw a hosted public snapshot").option("-y, --yes", "skip the explicit withdrawal prompt")
|
|
13568
|
+
).action(async (reference, opts) => {
|
|
13569
|
+
if (!opts.yes && !process.stdin.isTTY) die("non-interactive withdrawal requires --yes");
|
|
13570
|
+
if (!opts.yes && !await confirmPublicationAction("Withdraw this hosted snapshot now?"))
|
|
13571
|
+
die("withdrawal declined");
|
|
13572
|
+
const apiBase = normalizeUrl(resolveUrl(opts));
|
|
13573
|
+
const snapshotId = /^https?:/i.test(reference) ? parsePublicSnapshotReference(reference, apiBase) : reference;
|
|
13574
|
+
const result = await client(opts).withdrawPublication(snapshotId);
|
|
13575
|
+
if (opts.json) printJson(result);
|
|
13576
|
+
else console.log(`withdrew ${result.receipt.public_url}`);
|
|
13577
|
+
});
|
|
13578
|
+
withCommon(
|
|
13579
|
+
workflows.command("restore-publication <public-url-or-id>").description("Restore an owner-withdrawn public snapshot when host policy permits").option("-y, --yes", "skip the explicit restoration prompt")
|
|
13580
|
+
).action(async (reference, opts) => {
|
|
13581
|
+
if (!opts.yes && !process.stdin.isTTY) die("non-interactive restoration requires --yes");
|
|
13582
|
+
if (!opts.yes && !await confirmPublicationAction("Restore this hosted snapshot now?"))
|
|
13583
|
+
die("restoration declined");
|
|
13584
|
+
const apiBase = normalizeUrl(resolveUrl(opts));
|
|
13585
|
+
const snapshotId = /^https?:/i.test(reference) ? parsePublicSnapshotReference(reference, apiBase) : reference;
|
|
13586
|
+
const result = await client(opts).restorePublication(snapshotId);
|
|
13587
|
+
if (opts.json) printJson(result);
|
|
13588
|
+
else console.log(`restored ${result.receipt.public_url}`);
|
|
13589
|
+
});
|
|
13156
13590
|
withCommon(
|
|
13157
13591
|
workflows.command("export <workflow-id-or-unambiguous-name>").description("Export a canonical workflow package JSON document").option("--project <id-or-name>", "source project for selected project-bound configuration").option("--schedule <id>", "include this source schedule ID (repeatable)", collect2, []).option(
|
|
13158
13592
|
"--tier <state-ref=tier>",
|
|
@@ -13193,25 +13627,57 @@ function registerPackageCommands(workflows) {
|
|
|
13193
13627
|
}
|
|
13194
13628
|
);
|
|
13195
13629
|
withCommon(
|
|
13196
|
-
workflows.command("validate <file>").description("Validate a workflow package file (use - for stdin)")
|
|
13630
|
+
workflows.command("validate <file>").description("Validate a workflow package file (use - for stdin)").option("--public", "apply the stricter text-only public-hosting policy")
|
|
13197
13631
|
).action(async (path2, opts) => {
|
|
13198
|
-
const
|
|
13632
|
+
const api = client(opts);
|
|
13633
|
+
if (opts.public) {
|
|
13634
|
+
const result2 = await api.validatePublication({ document_json: readPackageSource(path2) });
|
|
13635
|
+
if (opts.json) printJson(result2);
|
|
13636
|
+
else {
|
|
13637
|
+
console.log(
|
|
13638
|
+
result2.valid ? "valid public workflow snapshot" : "invalid public workflow snapshot"
|
|
13639
|
+
);
|
|
13640
|
+
for (const diagnostic of result2.diagnostics)
|
|
13641
|
+
console.log(` ${diagnostic.path || "/"}: ${diagnostic.message}`);
|
|
13642
|
+
}
|
|
13643
|
+
if (!result2.valid) process.exitCode = 1;
|
|
13644
|
+
return;
|
|
13645
|
+
}
|
|
13646
|
+
const result = await api.validateLibrary({ document_json: readPackageSource(path2) });
|
|
13199
13647
|
if (opts.json) printJson(result);
|
|
13200
13648
|
else console.log(formatValidation(result));
|
|
13201
13649
|
if (!result.valid) process.exitCode = 1;
|
|
13202
13650
|
});
|
|
13203
13651
|
withCommon(
|
|
13204
|
-
workflows.command("preview <file>").description("Prepare and fully review a destination workflow package plan").option("--choices <file>", "destination choices JSON file").option("--plan-out <file>", "atomically save the signed plan for a later install")
|
|
13652
|
+
workflows.command("preview <file-or-public-url>").description("Prepare and fully review a destination workflow package plan").option("--choices <file>", "destination choices JSON file").option("--plan-out <file>", "atomically save the signed plan for a later install")
|
|
13205
13653
|
).action(async (path2, opts) => {
|
|
13206
|
-
const raw = readPackageSource(path2);
|
|
13207
13654
|
const choices = opts.choices ? readStrictObject(opts.choices, "choices file") : void 0;
|
|
13208
|
-
const
|
|
13209
|
-
|
|
13655
|
+
const apiBase = normalizeUrl(resolveUrl(opts));
|
|
13656
|
+
const api = client(opts);
|
|
13657
|
+
const source = publicationSource(path2, apiBase);
|
|
13658
|
+
let remoteSource;
|
|
13659
|
+
let plan;
|
|
13660
|
+
if (source.kind === "hosted") {
|
|
13661
|
+
plan = await api.prepareHostedWorkflowPackage(source.snapshotId, choices);
|
|
13662
|
+
} else {
|
|
13663
|
+
if (source.kind === "remote") {
|
|
13664
|
+
const fetched = await fetchPublicWorkflowPackage(path2);
|
|
13665
|
+
remoteSource = {
|
|
13666
|
+
url: fetched.sourceUrl,
|
|
13667
|
+
bytes_sha256: workflowPackageBytesSha256(fetched.raw)
|
|
13668
|
+
};
|
|
13669
|
+
plan = await api.prepareWorkflowPackage({ document_json: fetched.raw, choices });
|
|
13670
|
+
} else {
|
|
13671
|
+
const raw = readPackageSource(path2);
|
|
13672
|
+
plan = await api.prepareWorkflowPackage({ document_json: raw, choices });
|
|
13673
|
+
}
|
|
13674
|
+
}
|
|
13675
|
+
if (opts.planOut) saveWorkflowPackagePlan(opts.planOut, apiBase, plan, remoteSource);
|
|
13210
13676
|
if (opts.json) printJson(plan);
|
|
13211
13677
|
else console.log(formatWorkflowPackageReview(plan));
|
|
13212
13678
|
});
|
|
13213
13679
|
withCommon(
|
|
13214
|
-
workflows.command("install <file>").description("Install one exactly reviewed workflow package plan").option("--choices <file>", "destination choices JSON file (interactive preparation only)").option("--plan <file>", "signed plan saved by workflows preview").option("--confirm <plan-digest>", "exact reviewed plan digest (required outside a TTY)")
|
|
13680
|
+
workflows.command("install <file-or-public-url>").description("Install one exactly reviewed workflow package plan").option("--choices <file>", "destination choices JSON file (interactive preparation only)").option("--plan <file>", "signed plan saved by workflows preview").option("--confirm <plan-digest>", "exact reviewed plan digest (required outside a TTY)")
|
|
13215
13681
|
).action(
|
|
13216
13682
|
async (path2, opts) => {
|
|
13217
13683
|
if (path2 === "-" && process.stdin.isTTY)
|
|
@@ -13226,15 +13692,33 @@ function registerPackageCommands(workflows) {
|
|
|
13226
13692
|
die(
|
|
13227
13693
|
"stdin package input cannot also provide interactive confirmation; use preview --plan-out first"
|
|
13228
13694
|
);
|
|
13229
|
-
const { raw, document } = await localDocument(path2);
|
|
13230
13695
|
const apiBase = normalizeUrl(resolveUrl(opts));
|
|
13231
13696
|
const api = client(opts);
|
|
13697
|
+
const source = publicationSource(path2, apiBase);
|
|
13698
|
+
let raw;
|
|
13232
13699
|
let plan;
|
|
13233
13700
|
let planPath = opts.plan;
|
|
13234
13701
|
if (planPath) {
|
|
13235
13702
|
const saved = readWorkflowPackagePlan(planPath);
|
|
13236
13703
|
if (normalizeUrl(saved.api_base) !== apiBase)
|
|
13237
13704
|
die(`plan belongs to ${saved.api_base}, not ${apiBase}`);
|
|
13705
|
+
if (saved.remote_source) {
|
|
13706
|
+
if (source.kind !== "remote")
|
|
13707
|
+
die("saved remote plan must be installed from its public source URL");
|
|
13708
|
+
const fetched = await fetchPublicWorkflowPackage(path2);
|
|
13709
|
+
if (fetched.sourceUrl !== saved.remote_source.url || workflowPackageBytesSha256(fetched.raw) !== saved.remote_source.bytes_sha256)
|
|
13710
|
+
die("public source changed since preview; create and review a fresh plan");
|
|
13711
|
+
raw = fetched.raw;
|
|
13712
|
+
} else if (saved.plan.source?.kind === "hosted_publication") {
|
|
13713
|
+
if (source.kind !== "hosted" || source.snapshotId !== saved.plan.source.snapshot_id)
|
|
13714
|
+
die("saved hosted plan belongs to a different public snapshot");
|
|
13715
|
+
await api.getPublicSnapshotStatus(source.snapshotId);
|
|
13716
|
+
raw = canonicalWorkflowPackage(saved.plan.document);
|
|
13717
|
+
} else {
|
|
13718
|
+
if (source.kind !== "file") die("saved file plan must be installed from its file");
|
|
13719
|
+
raw = readPackageSource(path2);
|
|
13720
|
+
}
|
|
13721
|
+
const { document } = await packageDocument(raw);
|
|
13238
13722
|
if (saved.document_digest !== document.digest || saved.plan.document_digest !== document.digest || canonicalWorkflowPackage(saved.plan.document) !== canonicalWorkflowPackage(document))
|
|
13239
13723
|
die(
|
|
13240
13724
|
`package digest ${document.digest} does not match saved plan ${saved.document_digest}`
|
|
@@ -13242,9 +13726,25 @@ function registerPackageCommands(workflows) {
|
|
|
13242
13726
|
plan = saved.plan;
|
|
13243
13727
|
} else {
|
|
13244
13728
|
const choices = opts.choices ? readStrictObject(opts.choices, "choices file") : void 0;
|
|
13245
|
-
|
|
13246
|
-
|
|
13247
|
-
|
|
13729
|
+
let remoteSource;
|
|
13730
|
+
if (source.kind === "hosted") {
|
|
13731
|
+
plan = await api.prepareHostedWorkflowPackage(source.snapshotId, choices);
|
|
13732
|
+
raw = canonicalWorkflowPackage(plan.document);
|
|
13733
|
+
} else if (source.kind === "remote") {
|
|
13734
|
+
const fetched = await fetchPublicWorkflowPackage(path2);
|
|
13735
|
+
raw = fetched.raw;
|
|
13736
|
+
await packageDocument(raw);
|
|
13737
|
+
remoteSource = {
|
|
13738
|
+
url: fetched.sourceUrl,
|
|
13739
|
+
bytes_sha256: workflowPackageBytesSha256(raw)
|
|
13740
|
+
};
|
|
13741
|
+
plan = await api.prepareWorkflowPackage({ document_json: raw, choices });
|
|
13742
|
+
} else {
|
|
13743
|
+
({ raw } = await localDocument(path2));
|
|
13744
|
+
plan = await api.prepareWorkflowPackage({ document_json: raw, choices });
|
|
13745
|
+
}
|
|
13746
|
+
planPath = source.kind === "file" ? `${path2}.plan.json` : "workflow-publication.plan.json";
|
|
13747
|
+
saveWorkflowPackagePlan(planPath, apiBase, plan, remoteSource);
|
|
13248
13748
|
console.error(`saved retryable signed plan to ${planPath}`);
|
|
13249
13749
|
}
|
|
13250
13750
|
if (opts.confirm !== void 0 && opts.confirm !== plan.plan_digest)
|