tines 0.0.235 → 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.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/dist/index.js +516 -16
  3. 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 !== outerKeys.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" || Object.keys(saved.plan).length !== planKeys.length || !Object.keys(saved.plan).every((key) => planKeys.includes(key))) {
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 result = await client(opts).validateLibrary({ document_json: readPackageSource(path2) });
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 plan = await client(opts).prepareWorkflowPackage({ document_json: raw, choices });
13209
- if (opts.planOut) saveWorkflowPackagePlan(opts.planOut, resolveUrl(opts), plan);
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
- plan = await api.prepareWorkflowPackage({ document_json: raw, choices });
13246
- planPath = `${path2}.plan.json`;
13247
- saveWorkflowPackagePlan(planPath, apiBase, plan);
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.235",
3
+ "version": "0.0.237",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",