@hasna/skills 0.5.3 → 0.5.5
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 +72 -7
- package/bin/index.js +1429 -209
- package/bin/mcp.js +1545 -40
- package/bin/migrate.js +13 -4
- package/bin/server.js +81 -19
- package/bin/worker.js +37 -7
- package/dist/cli/commands/private-publications.d.ts +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +756 -24
- package/dist/lib/fleet-credentials.d.ts +6 -1
- package/dist/lib/home-adoption.d.ts +2 -7
- package/dist/lib/mcp-contracts.d.ts +1 -0
- package/dist/lib/private-publication-customer.d.ts +10 -0
- package/dist/lib/private-publication-recovery.d.ts +43 -0
- package/dist/lib/remote-account.d.ts +5 -0
- package/dist/lib/remote-auth.d.ts +3 -0
- package/dist/lib/remote-client.d.ts +9 -2
- package/dist/lib/remote-private-publications.d.ts +74 -0
- package/dist/lib/remote-quote-errors.d.ts +12 -0
- package/dist/mcp/private-publication-tools.d.ts +2 -0
- package/dist/sdk/execution/dispatchers/ecs.d.ts +25 -14
- package/dist/sdk/index.d.ts +4 -1
- package/dist/sdk/index.js +938 -164
- package/dist/server/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9528,7 +9528,9 @@ function normalizeSkillsApiOrigin(apiUrl) {
|
|
|
9528
9528
|
throw new SkillsFleetCredentialError("A Skills API URL must use HTTPS (or loopback HTTP), without credentials, query or fragment", "INVALID_API_URL");
|
|
9529
9529
|
}
|
|
9530
9530
|
const pathname = url.pathname.replace(/\/+$/, "");
|
|
9531
|
-
if (
|
|
9531
|
+
if (url.origin === "https://api.hasna.com" && pathname === "/skills/v1") {
|
|
9532
|
+
url.pathname = "/skills";
|
|
9533
|
+
} else if (pathname === "/api" || pathname === "/api/v1") {
|
|
9532
9534
|
url.pathname = "/";
|
|
9533
9535
|
} else if (pathname.endsWith("/api/v1")) {
|
|
9534
9536
|
url.pathname = pathname.slice(0, -"/api/v1".length) || "/";
|
|
@@ -9537,6 +9539,21 @@ function normalizeSkillsApiOrigin(apiUrl) {
|
|
|
9537
9539
|
}
|
|
9538
9540
|
return url.toString().replace(/\/+$/, "");
|
|
9539
9541
|
}
|
|
9542
|
+
function skillsApiRequestUrl(apiUrl, route) {
|
|
9543
|
+
const origin = normalizeSkillsApiOrigin(apiUrl);
|
|
9544
|
+
if (!route.startsWith("/api/") || route.includes("#") || route.includes("\\")) {
|
|
9545
|
+
throw new SkillsFleetCredentialError("Invalid Skills API route", "INVALID_API_URL");
|
|
9546
|
+
}
|
|
9547
|
+
if (origin === "https://api.hasna.com/skills") {
|
|
9548
|
+
if (route === "/api/auth/whoami")
|
|
9549
|
+
return `${origin}/v1/auth/whoami`;
|
|
9550
|
+
if (!route.startsWith("/api/v1/")) {
|
|
9551
|
+
throw new SkillsFleetCredentialError("The internal Skills gateway has no established login contract yet. Select an explicitly configured instance with supported authentication.", "GATEWAY_AUTH_UNAVAILABLE");
|
|
9552
|
+
}
|
|
9553
|
+
return `${origin}${route.slice("/api".length)}`;
|
|
9554
|
+
}
|
|
9555
|
+
return `${origin}${route}`;
|
|
9556
|
+
}
|
|
9540
9557
|
function configuredSkillsApiUrl(env = process.env, keychain, profile) {
|
|
9541
9558
|
const declared = SKILLS_API_URL_ENV_KEYS.filter((key) => env[key] !== undefined).map((key) => ({ key, value: env[key] }));
|
|
9542
9559
|
for (const entry of declared) {
|
|
@@ -9889,7 +9906,14 @@ function getConfiguredApiUrl(env = process.env) {
|
|
|
9889
9906
|
}
|
|
9890
9907
|
function buildSkillsApiUrl(apiUrl, endpoint = "/skills") {
|
|
9891
9908
|
const url = new URL(apiUrl);
|
|
9909
|
+
if (url.origin === "https://api.hasna.com" && /^\/skills\/(?:api\/)?v1\/skills\/?$/.test(url.pathname)) {
|
|
9910
|
+
url.pathname = "/skills";
|
|
9911
|
+
apiUrl = url.toString();
|
|
9912
|
+
}
|
|
9892
9913
|
const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
9914
|
+
if (normalizeSkillsApiOrigin(apiUrl) === "https://api.hasna.com/skills") {
|
|
9915
|
+
return skillsApiRequestUrl(apiUrl, `/api/v1${cleanEndpoint}`);
|
|
9916
|
+
}
|
|
9893
9917
|
const pathname = url.pathname.replace(/\/+$/, "");
|
|
9894
9918
|
const apiBase = /\/api(?:\/v1)?\/skills$/.test(pathname) ? pathname.slice(0, -"/skills".length) : pathname;
|
|
9895
9919
|
if (/\/api(?:\/v1)?$/.test(apiBase)) {
|
|
@@ -10990,8 +11014,17 @@ function creditCount(value) {
|
|
|
10990
11014
|
}
|
|
10991
11015
|
return value;
|
|
10992
11016
|
}
|
|
11017
|
+
function runQuoteReceipt(value) {
|
|
11018
|
+
if (value === undefined)
|
|
11019
|
+
return;
|
|
11020
|
+
if (typeof value !== "string" || !value.length || Buffer.byteLength(value, "utf8") > 4096) {
|
|
11021
|
+
throw new Error("Invalid quote receipt");
|
|
11022
|
+
}
|
|
11023
|
+
return value;
|
|
11024
|
+
}
|
|
10993
11025
|
function parseRemoteRunQuote(value) {
|
|
10994
11026
|
const quote = object(value);
|
|
11027
|
+
runQuoteReceipt(quote.quoteReceipt);
|
|
10995
11028
|
const pricing = object(quote.pricing);
|
|
10996
11029
|
if (typeof quote.skill !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(quote.skill))
|
|
10997
11030
|
throw new Error("Invalid quoted skill");
|
|
@@ -11138,6 +11171,69 @@ function parseUpdatedWorkspace(value) {
|
|
|
11138
11171
|
return { organization: { id: organization2.id, slug: organization2.slug, name: organization2.name } };
|
|
11139
11172
|
}
|
|
11140
11173
|
|
|
11174
|
+
// src/lib/remote-quote-errors.ts
|
|
11175
|
+
var quoteUnavailableMessages = Object.freeze({
|
|
11176
|
+
HOSTED_PROVIDER_UNAVAILABLE: "Hosted execution is temporarily unavailable on this Skills instance.",
|
|
11177
|
+
HOSTED_CONNECTORS_UNAVAILABLE: "Hosted connector execution is unavailable on this Skills instance.",
|
|
11178
|
+
SKILL_IMPLEMENTATION_UNAVAILABLE: "This skill has no hosted execution implementation.",
|
|
11179
|
+
HOSTED_PRICING_UNAVAILABLE: "Hosted execution is unavailable while this skill's pricing is reviewed.",
|
|
11180
|
+
RUNTIME_ALLOWLIST_REQUIRED: "Hosted execution is unavailable until this Skills instance enables its skill catalog.",
|
|
11181
|
+
RUNTIME_SKILL_NOT_ALLOWED: "This skill is not enabled for hosted execution on this Skills instance."
|
|
11182
|
+
});
|
|
11183
|
+
async function readQuoteUnavailableCode(response) {
|
|
11184
|
+
const maximum = 16 * 1024;
|
|
11185
|
+
const length = response.headers.get("content-length");
|
|
11186
|
+
if (response.status !== 503 || response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json" || length !== null && (!/^\d+$/.test(length) || Number(length) > maximum)) {
|
|
11187
|
+
response.body?.cancel().catch(() => {});
|
|
11188
|
+
return null;
|
|
11189
|
+
}
|
|
11190
|
+
const reader = response.body?.getReader();
|
|
11191
|
+
if (!reader)
|
|
11192
|
+
return null;
|
|
11193
|
+
let timer;
|
|
11194
|
+
let deadlineExceeded = false;
|
|
11195
|
+
const expired = Symbol("quote body deadline");
|
|
11196
|
+
const deadline = new Promise((resolve2) => {
|
|
11197
|
+
timer = setTimeout(() => {
|
|
11198
|
+
deadlineExceeded = true;
|
|
11199
|
+
resolve2(expired);
|
|
11200
|
+
reader.cancel().catch(() => {});
|
|
11201
|
+
}, 1500);
|
|
11202
|
+
});
|
|
11203
|
+
const chunks = [];
|
|
11204
|
+
let size = 0;
|
|
11205
|
+
try {
|
|
11206
|
+
while (true) {
|
|
11207
|
+
const next = await Promise.race([reader.read(), deadline]);
|
|
11208
|
+
if (next === expired || deadlineExceeded)
|
|
11209
|
+
return null;
|
|
11210
|
+
if (next.done)
|
|
11211
|
+
break;
|
|
11212
|
+
size += next.value.byteLength;
|
|
11213
|
+
if (size > maximum)
|
|
11214
|
+
return null;
|
|
11215
|
+
chunks.push(next.value);
|
|
11216
|
+
}
|
|
11217
|
+
const bytes = new Uint8Array(size);
|
|
11218
|
+
let offset = 0;
|
|
11219
|
+
for (const chunk of chunks) {
|
|
11220
|
+
bytes.set(chunk, offset);
|
|
11221
|
+
offset += chunk.byteLength;
|
|
11222
|
+
}
|
|
11223
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
11224
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
11225
|
+
return null;
|
|
11226
|
+
const code = value.code;
|
|
11227
|
+
return typeof code === "string" && Object.hasOwn(quoteUnavailableMessages, code) ? code : null;
|
|
11228
|
+
} catch {
|
|
11229
|
+
return null;
|
|
11230
|
+
} finally {
|
|
11231
|
+
clearTimeout(timer);
|
|
11232
|
+
reader.cancel().catch(() => {});
|
|
11233
|
+
reader.releaseLock();
|
|
11234
|
+
}
|
|
11235
|
+
}
|
|
11236
|
+
|
|
11141
11237
|
// src/lib/remote-client.ts
|
|
11142
11238
|
class RemoteRouteUnsupportedError extends Error {
|
|
11143
11239
|
path;
|
|
@@ -11163,6 +11259,18 @@ class RemoteRequestError extends Error {
|
|
|
11163
11259
|
}
|
|
11164
11260
|
}
|
|
11165
11261
|
|
|
11262
|
+
class RemoteQuoteUnavailableError extends RemoteRequestError {
|
|
11263
|
+
code;
|
|
11264
|
+
constructor(path, code) {
|
|
11265
|
+
super(path, 503);
|
|
11266
|
+
this.code = code;
|
|
11267
|
+
if (!Object.hasOwn(quoteUnavailableMessages, code))
|
|
11268
|
+
throw new Error("Unknown quote refusal code");
|
|
11269
|
+
this.name = "RemoteQuoteUnavailableError";
|
|
11270
|
+
this.message = quoteUnavailableMessages[code];
|
|
11271
|
+
}
|
|
11272
|
+
}
|
|
11273
|
+
|
|
11166
11274
|
class RemoteWorkspaceMemberError extends RemoteRequestError {
|
|
11167
11275
|
code;
|
|
11168
11276
|
constructor(path, code) {
|
|
@@ -11201,7 +11309,7 @@ class RemoteSkillsClient {
|
|
|
11201
11309
|
this.apiUrl = normalizeSkillsApiOrigin(apiUrl);
|
|
11202
11310
|
}
|
|
11203
11311
|
async request(path, options) {
|
|
11204
|
-
return fetch(
|
|
11312
|
+
return fetch(skillsApiRequestUrl(this.apiUrl, path), {
|
|
11205
11313
|
...options,
|
|
11206
11314
|
redirect: "error",
|
|
11207
11315
|
credentials: "omit",
|
|
@@ -11224,6 +11332,11 @@ class RemoteSkillsClient {
|
|
|
11224
11332
|
throw new RemoteRouteUnsupportedError(routePath, response.status, this.apiUrl);
|
|
11225
11333
|
}
|
|
11226
11334
|
if (!response.ok) {
|
|
11335
|
+
if (opts.quoteRefusal && options?.method === "POST" && /^\/api\/v1\/skills\/[^/?#]+\/quote$/.test(routePath) && response.status === 503) {
|
|
11336
|
+
const code = await readQuoteUnavailableCode(response);
|
|
11337
|
+
if (code)
|
|
11338
|
+
throw new RemoteQuoteUnavailableError(routePath, code);
|
|
11339
|
+
}
|
|
11227
11340
|
if (path === "/api/v1/billing/checkout" && options?.method === "POST" && response.status === 503 && await responseBodyCarriesCode(response, ["SUBSCRIPTION_CHECKOUT_UNAVAILABLE"])) {
|
|
11228
11341
|
throw new RemoteCapabilityUnavailableError;
|
|
11229
11342
|
}
|
|
@@ -11256,6 +11369,7 @@ class RemoteSkillsClient {
|
|
|
11256
11369
|
return { status: res.status, body };
|
|
11257
11370
|
}
|
|
11258
11371
|
async submitRun(slug, input, args, approval = {}) {
|
|
11372
|
+
const quoteReceipt = runQuoteReceipt(approval.quoteReceipt);
|
|
11259
11373
|
if (approval.idempotencyKey !== undefined && !/^[A-Za-z0-9._:-]{1,128}$/.test(approval.idempotencyKey))
|
|
11260
11374
|
throw new Error("Idempotency key must be 1-128 URL-safe characters");
|
|
11261
11375
|
if (approval.maxCostCents !== undefined)
|
|
@@ -11272,16 +11386,21 @@ class RemoteSkillsClient {
|
|
|
11272
11386
|
...approval.maxCredits !== undefined ? { maxCredits: approval.maxCredits } : {},
|
|
11273
11387
|
...approval.maxCostCents !== undefined ? { maxCostCents: approval.maxCostCents } : {},
|
|
11274
11388
|
...approval.idempotencyKey !== undefined ? { idempotencyKey: approval.idempotencyKey } : {},
|
|
11275
|
-
...approval.inputFiles !== undefined ? { files: approval.inputFiles } : {}
|
|
11389
|
+
...approval.inputFiles !== undefined ? { files: approval.inputFiles } : {},
|
|
11390
|
+
...quoteReceipt !== undefined ? { quoteReceipt } : {}
|
|
11276
11391
|
})
|
|
11277
11392
|
});
|
|
11393
|
+
if (!res.ok) {
|
|
11394
|
+
res.body?.cancel().catch(() => {});
|
|
11395
|
+
throw new RemoteRequestError(`/api/v1/runs/${encodeURIComponent(slug)}`, res.status);
|
|
11396
|
+
}
|
|
11278
11397
|
return normalizeRemoteSkillRunContract(await res.json(), slug);
|
|
11279
11398
|
}
|
|
11280
|
-
async quoteRun(slug, input = {}, args = []) {
|
|
11399
|
+
async quoteRun(slug, input = {}, args = [], files) {
|
|
11281
11400
|
const response = await this.requestNewRoute(`/api/v1/skills/${encodeURIComponent(slug)}/quote`, {
|
|
11282
11401
|
method: "POST",
|
|
11283
|
-
body: JSON.stringify({ input, args })
|
|
11284
|
-
});
|
|
11402
|
+
body: JSON.stringify({ input, args, ...files === undefined ? {} : { files } })
|
|
11403
|
+
}, { quoteRefusal: true });
|
|
11285
11404
|
return parseRemoteRunQuote(await response.json());
|
|
11286
11405
|
}
|
|
11287
11406
|
getCapabilities() {
|
|
@@ -11296,17 +11415,24 @@ class RemoteSkillsClient {
|
|
|
11296
11415
|
return this.capabilities;
|
|
11297
11416
|
}
|
|
11298
11417
|
async submitQuotedRun(slug, input = {}, args = [], approval = {}) {
|
|
11418
|
+
runQuoteReceipt(approval.quoteReceipt);
|
|
11419
|
+
({ input, args, approval } = JSON.parse(JSON.stringify({ input, args, approval })));
|
|
11299
11420
|
const maximum = creditCount(approval.maxCredits ?? approval.maxCostCents ?? 0);
|
|
11300
11421
|
if (approval.maxCostCents !== undefined && approval.maxCostCents !== maximum)
|
|
11301
11422
|
throw new Error("Credit approval fields disagree");
|
|
11302
|
-
const quote = await this.quoteRun(slug, input, args);
|
|
11303
|
-
if (quote.pricing.costCents > maximum)
|
|
11423
|
+
const quote = approval.quoteReceipt === undefined ? await this.quoteRun(slug, input, args, approval.inputFiles?.length ? approval.inputFiles : undefined) : undefined;
|
|
11424
|
+
if (quote && quote.pricing.costCents > maximum)
|
|
11304
11425
|
throw new RemoteCreditApprovalError(quote.pricing.costCents, maximum);
|
|
11305
11426
|
const capabilities = await this.getCapabilities();
|
|
11306
11427
|
if (!capabilities.capabilities.includes("runs.submit") || capabilities.billing?.boundedRunApproval !== true || capabilities.billing.unit !== "credits") {
|
|
11307
11428
|
throw new Error("The configured server does not support bounded credit approval; refusing remote submission");
|
|
11308
11429
|
}
|
|
11309
|
-
return this.submitRun(quote
|
|
11430
|
+
return this.submitRun(quote?.skill ?? slug, input, args, {
|
|
11431
|
+
...approval,
|
|
11432
|
+
maxCredits: maximum,
|
|
11433
|
+
maxCostCents: maximum,
|
|
11434
|
+
...(quote?.quoteReceipt ?? approval.quoteReceipt) === undefined ? {} : { quoteReceipt: quote?.quoteReceipt ?? approval.quoteReceipt }
|
|
11435
|
+
});
|
|
11310
11436
|
}
|
|
11311
11437
|
async getIdentity() {
|
|
11312
11438
|
return (await this.requestNewRoute("/api/auth/whoami")).json();
|
|
@@ -11594,6 +11720,10 @@ class RemoteSkillsClient {
|
|
|
11594
11720
|
return { id: artifactId, fileName: String(artifact.fileName ?? artifactId), bytes, byteSize: bytes.byteLength, sha256: artifact.sha256 };
|
|
11595
11721
|
}
|
|
11596
11722
|
async submitQuotedRunWithFiles(slug, input, args, files, approval = {}) {
|
|
11723
|
+
runQuoteReceipt(approval.quoteReceipt);
|
|
11724
|
+
({ input, args, approval } = JSON.parse(JSON.stringify({ input, args, approval })));
|
|
11725
|
+
describeRemoteFiles(files);
|
|
11726
|
+
files = files.map((file) => ({ name: file.name, contentType: file.contentType, bytes: new Uint8Array(file.bytes) }));
|
|
11597
11727
|
const inputFiles = describeRemoteFiles(files);
|
|
11598
11728
|
if (files.length && !(await this.getCapabilities()).capabilities.includes("runs.uploads"))
|
|
11599
11729
|
throw new Error("The configured server does not support input uploads");
|
|
@@ -11657,7 +11787,7 @@ class RemoteSkillsClient {
|
|
|
11657
11787
|
const headers = { Authorization: `Bearer ${this.apiKey}` };
|
|
11658
11788
|
if (ifMatch)
|
|
11659
11789
|
headers["If-Match"] = ifMatch;
|
|
11660
|
-
return fetch(
|
|
11790
|
+
return fetch(skillsApiRequestUrl(this.apiUrl, "/api/v1/skills"), {
|
|
11661
11791
|
method: "POST",
|
|
11662
11792
|
headers,
|
|
11663
11793
|
body: form,
|
|
@@ -13206,7 +13336,7 @@ import { dirname as dirname7, relative as relative4 } from "path";
|
|
|
13206
13336
|
// package.json
|
|
13207
13337
|
var package_default = {
|
|
13208
13338
|
name: "@hasna/skills",
|
|
13209
|
-
version: "0.5.
|
|
13339
|
+
version: "0.5.5",
|
|
13210
13340
|
description: "Skills library for AI coding agents",
|
|
13211
13341
|
type: "module",
|
|
13212
13342
|
bin: {
|
|
@@ -13940,7 +14070,7 @@ var toolContracts = [
|
|
|
13940
14070
|
name: "run_skill",
|
|
13941
14071
|
title: "Run Skill",
|
|
13942
14072
|
description: "Run a skill locally or through a configured remote runner. Returns compact stdout/stderr previews and run summaries by default; pass detail:true for full records.",
|
|
13943
|
-
params: ["name", "input?", "args?", "detail?", "remote?", "maxCredits?", "maxCostCents?", "idempotency_key?", "files?"],
|
|
14073
|
+
params: ["name", "input?", "args?", "detail?", "remote?", "maxCredits?", "maxCostCents?", "quoteReceipt?", "idempotency_key?", "files?"],
|
|
13944
14074
|
category: "execution",
|
|
13945
14075
|
sideEffects: "local-process-or-remote-run",
|
|
13946
14076
|
stable: true,
|
|
@@ -13952,6 +14082,7 @@ var toolContracts = [
|
|
|
13952
14082
|
remote: { type: "boolean", description: "Use the configured server catalog." },
|
|
13953
14083
|
maxCredits: { type: "integer", minimum: 0, description: "Maximum explicitly approved integer credits; omitted permits only free remote runs." },
|
|
13954
14084
|
maxCostCents: { type: "integer", minimum: 0, description: "Legacy alias for maxCredits; both must agree." },
|
|
14085
|
+
quoteReceipt: { type: "string", minLength: 1, maxLength: 4096, description: "Opaque approved quote receipt, at most 4096 UTF-8 bytes. Preserve it and the quoted input/args unchanged; never refresh after confirmation." },
|
|
13955
14086
|
idempotency_key: { type: "string", pattern: "^[A-Za-z0-9._:-]{1,128}$", description: "Stable retry key for the same remote submission." },
|
|
13956
14087
|
files: { type: "array", maxItems: 10, items: objectSchema({ name: stringSchema("Safe basename"), base64: { type: "string", maxLength: 1398104 }, contentType: stringSchema("MIME type") }, ["name", "base64"]), description: "Inline remote inputs, at most 1 MiB combined." }
|
|
13957
14088
|
}, ["name"]),
|
|
@@ -14235,12 +14366,17 @@ remoteCustomerContracts.push({
|
|
|
14235
14366
|
name: "quote_skill",
|
|
14236
14367
|
title: "Quote Remote Skill",
|
|
14237
14368
|
description: "Get a server credit quote without submitting a run.",
|
|
14238
|
-
params: ["name", "input?", "args?"],
|
|
14369
|
+
params: ["name", "input?", "args?", "files?"],
|
|
14239
14370
|
category: "execution",
|
|
14240
14371
|
sideEffects: "none",
|
|
14241
14372
|
stable: true,
|
|
14242
|
-
inputSchema: objectSchema({
|
|
14243
|
-
|
|
14373
|
+
inputSchema: objectSchema({
|
|
14374
|
+
name: skillNameInput,
|
|
14375
|
+
input: runInputSchema,
|
|
14376
|
+
args: runArgsSchema,
|
|
14377
|
+
files: { type: "array", maxItems: 10, items: objectSchema({ name: stringSchema("Safe basename"), base64: { type: "string", maxLength: 1398104 }, contentType: stringSchema("MIME type") }, ["name", "base64"]), description: "Same inline files to submit after approval, at most 1 MiB combined." }
|
|
14378
|
+
}, ["name"]),
|
|
14379
|
+
outputSchema: objectSchema({ skill: stringSchema("Canonical server skill."), pricing: objectSchema({}, [], "Quoted integer credits.", true), quoteReceipt: { type: "string", minLength: 1, maxLength: 4096, description: "Opaque server quote binding, at most 4096 UTF-8 bytes; preserve verbatim for approval." } }, ["skill", "pricing"], undefined, true)
|
|
14244
14380
|
});
|
|
14245
14381
|
remoteCustomerContracts.push({
|
|
14246
14382
|
name: "download_run_artifact",
|
|
@@ -14253,7 +14389,47 @@ remoteCustomerContracts.push({
|
|
|
14253
14389
|
inputSchema: objectSchema({ run_id: stringSchema("Run identifier."), artifact_id: stringSchema("Artifact identifier.") }, ["run_id", "artifact_id"]),
|
|
14254
14390
|
outputSchema: objectSchema({ id: stringSchema("Artifact identifier."), fileName: stringSchema("Artifact file name."), base64: stringSchema("Verified bytes."), sha256: stringSchema("SHA256 digest."), byteSize: { type: "integer", minimum: 0 } }, ["id", "fileName", "base64", "sha256", "byteSize"])
|
|
14255
14391
|
});
|
|
14256
|
-
var
|
|
14392
|
+
var publicationUuidSchema = { type: "string", pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" };
|
|
14393
|
+
var publicationVerification = {
|
|
14394
|
+
email: { type: "string", format: "email", maxLength: 254 },
|
|
14395
|
+
code: { type: "string", pattern: "^\\d{6}$" },
|
|
14396
|
+
userId: publicationUuidSchema,
|
|
14397
|
+
membershipId: publicationUuidSchema,
|
|
14398
|
+
recoveryDirectory: { type: "string", maxLength: 4096, description: "Absolute host-local recovery directory without symbolic links." }
|
|
14399
|
+
};
|
|
14400
|
+
var privatePublicationContracts = [
|
|
14401
|
+
{ name: "publish_private_skill", title: "Publish private skill", extras: {
|
|
14402
|
+
directory: { type: "string", maxLength: 4096, description: "Absolute local skill source directory." },
|
|
14403
|
+
skillId: publicationUuidSchema,
|
|
14404
|
+
expectedCurrentVersionId: { oneOf: [publicationUuidSchema, { type: "null" }] },
|
|
14405
|
+
idempotencyKey: publicationUuidSchema,
|
|
14406
|
+
confirm: { const: true },
|
|
14407
|
+
waitMs: { type: "integer", minimum: 0, maximum: 300000 }
|
|
14408
|
+
}, required: ["directory", "skillId", "expectedCurrentVersionId", "confirm"] },
|
|
14409
|
+
{ name: "get_private_publication", title: "Get private publication", extras: {}, required: [] },
|
|
14410
|
+
{ name: "resume_private_publication", title: "Resume private publication", extras: { confirm: { const: true }, waitMs: { type: "integer", minimum: 0, maximum: 300000 } }, required: ["confirm"] },
|
|
14411
|
+
{ name: "cancel_private_publication", title: "Cancel private publication", extras: { confirm: { const: true } }, required: ["confirm"] }
|
|
14412
|
+
].map((operation) => ({
|
|
14413
|
+
name: operation.name,
|
|
14414
|
+
title: operation.title,
|
|
14415
|
+
description: "Manage private source publication with fresh workspace verification and durable host-local recovery. Upload consent and current version comparison are explicit; private execution remains unavailable.",
|
|
14416
|
+
params: [...Object.keys(publicationVerification), ...Object.keys(operation.extras)],
|
|
14417
|
+
category: "storage",
|
|
14418
|
+
sideEffects: "filesystem",
|
|
14419
|
+
stable: true,
|
|
14420
|
+
inputSchema: objectSchema({ ...publicationVerification, ...operation.extras }, [...Object.keys(publicationVerification), ...operation.required]),
|
|
14421
|
+
outputSchema: objectSchema({
|
|
14422
|
+
recoveryDirectory: { type: "string" },
|
|
14423
|
+
skillId: publicationUuidSchema,
|
|
14424
|
+
intentId: { oneOf: [publicationUuidSchema, { type: "null" }] },
|
|
14425
|
+
state: { type: "string" },
|
|
14426
|
+
versionId: { oneOf: [publicationUuidSchema, { type: "null" }] },
|
|
14427
|
+
committed: { type: "boolean" },
|
|
14428
|
+
executionEnabled: { const: false },
|
|
14429
|
+
nextAction: { type: "string" }
|
|
14430
|
+
}, ["recoveryDirectory", "skillId", "intentId", "state", "versionId", "committed", "executionEnabled", "nextAction"])
|
|
14431
|
+
}));
|
|
14432
|
+
var contracts = [...toolContracts, ...remoteCustomerContracts, ...privatePublicationContracts].sort((a, b) => a.name.localeCompare(b.name));
|
|
14257
14433
|
var resourceContracts = [
|
|
14258
14434
|
{
|
|
14259
14435
|
uri: "skills://mcp/contracts",
|
|
@@ -15740,7 +15916,7 @@ async function requestInvitationEmail(origin, action, input) {
|
|
|
15740
15916
|
}
|
|
15741
15917
|
const body = JSON.stringify({ invitationId: value.invitationId, token: value.token, challengeId: value.challengeId, ...action === "accept" ? { code: value.code } : {} });
|
|
15742
15918
|
try {
|
|
15743
|
-
const response = await fetch(
|
|
15919
|
+
const response = await fetch(skillsApiRequestUrl(target, `/api/v1/account/invitations/email-${action}`), {
|
|
15744
15920
|
method: "POST",
|
|
15745
15921
|
headers: { "Content-Type": "application/json" },
|
|
15746
15922
|
body,
|
|
@@ -15771,6 +15947,312 @@ async function requestInvitationEmail(origin, action, input) {
|
|
|
15771
15947
|
throw new RemoteInvitationEmailUnconfirmedError(action);
|
|
15772
15948
|
}
|
|
15773
15949
|
|
|
15950
|
+
// src/lib/remote-private-publications.ts
|
|
15951
|
+
import { createHash as createHash9 } from "crypto";
|
|
15952
|
+
var PRIVATE_PUBLICATION_MAX_BYTES = 16 * 1024 * 1024;
|
|
15953
|
+
var failures = {
|
|
15954
|
+
INVALID_REQUEST: [400, "The publication request is invalid."],
|
|
15955
|
+
SESSION_EXPIRED: [401, "Sign in again to manage this publication."],
|
|
15956
|
+
ACCOUNT_UNAVAILABLE: [403, "The account is unavailable."],
|
|
15957
|
+
INTERACTIVE_SESSION_REQUIRED: [403, "An interactive account session is required."],
|
|
15958
|
+
PUBLICATION_FORBIDDEN: [403, "This session cannot manage the publication."],
|
|
15959
|
+
PUBLICATION_ENTITLEMENT_REQUIRED: [403, "The workspace is not entitled to publish private skills."],
|
|
15960
|
+
PUBLICATION_UNAVAILABLE: [404, "The publication is unavailable to this session."],
|
|
15961
|
+
MANIFEST_NAME_MISMATCH: [409, "The manifest name does not match the selected skill."],
|
|
15962
|
+
IDEMPOTENCY_CONFLICT: [409, "This request key already identifies different publication bytes. Use the saved recovery directory."],
|
|
15963
|
+
CURRENT_VERSION_CHANGED: [409, "The current version changed. Inspect it before explicitly starting another publication."],
|
|
15964
|
+
VERSION_EXISTS: [409, "This version already exists."],
|
|
15965
|
+
VERSION_RESERVED: [409, "This version is reserved by another publication."],
|
|
15966
|
+
PUBLICATION_COMMITTED: [409, "The publication is already committed."],
|
|
15967
|
+
PUBLICATION_UPLOAD_UNAVAILABLE: [409, "This publication cannot receive another upload."],
|
|
15968
|
+
PUBLICATION_LIMIT: [429, "The workspace publication limit has been reached."],
|
|
15969
|
+
PUBLICATION_BUSY: [503, "The publication is busy. Reconcile the saved intent before retrying."],
|
|
15970
|
+
PUBLICATION_UNCERTAIN: [503, "The publication outcome is uncertain. Reconcile the saved intent."],
|
|
15971
|
+
PUBLICATION_CAPABILITY_UNAVAILABLE: [503, "Private publishing is not enabled on this server."],
|
|
15972
|
+
PUBLICATION_SIGNING_UNAVAILABLE: [503, "Upload authorization is temporarily unavailable. Keep the same intent."]
|
|
15973
|
+
};
|
|
15974
|
+
|
|
15975
|
+
class PrivatePublicationError extends Error {
|
|
15976
|
+
code;
|
|
15977
|
+
uncertain;
|
|
15978
|
+
status;
|
|
15979
|
+
constructor(code, message, uncertain = false, status) {
|
|
15980
|
+
super(message);
|
|
15981
|
+
this.code = code;
|
|
15982
|
+
this.uncertain = uncertain;
|
|
15983
|
+
this.status = status;
|
|
15984
|
+
this.name = "PrivatePublicationError";
|
|
15985
|
+
}
|
|
15986
|
+
}
|
|
15987
|
+
var bad = () => {
|
|
15988
|
+
throw new PrivatePublicationError("INVALID_PUBLICATION_INPUT", "Invalid publication input or recovery data.");
|
|
15989
|
+
};
|
|
15990
|
+
var invalid3 = () => {
|
|
15991
|
+
throw new PrivatePublicationError("INVALID_PUBLICATION_RESPONSE", "The server returned an invalid publication result.");
|
|
15992
|
+
};
|
|
15993
|
+
var publicationUuid = (v) => typeof v === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(v);
|
|
15994
|
+
var hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
|
|
15995
|
+
var record5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
15996
|
+
var exact = (v, keys) => record5(v) && Object.keys(v).sort().join(",") === keys.sort().join(",");
|
|
15997
|
+
var date = (v) => typeof v === "string" && /^\d{4}-\d\d-\d\dT[0-9:.]+(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.test(v) && Number.isFinite(Date.parse(v));
|
|
15998
|
+
var publicationSha256 = (bytes) => createHash9("sha256").update(bytes).digest("hex");
|
|
15999
|
+
function checkedPublicationDeclaration(value) {
|
|
16000
|
+
if (!exact(value, ["idempotencyKey", "version", "expectedCurrentVersionId", "manifestText", "archiveSha256", "archiveByteSize"]) || !publicationUuid(value.idempotencyKey) || !(value.expectedCurrentVersionId === null || publicationUuid(value.expectedCurrentVersionId)) || typeof value.version !== "string" || !value.version || value.version.length > 128 || /[\p{Cc}\p{Cs}]/u.test(value.version) || typeof value.manifestText !== "string" || Buffer.byteLength(value.manifestText) > 16384 || !hash(value.archiveSha256) || !Number.isSafeInteger(value.archiveByteSize) || Number(value.archiveByteSize) < 1 || Number(value.archiveByteSize) > PRIVATE_PUBLICATION_MAX_BYTES)
|
|
16001
|
+
return bad();
|
|
16002
|
+
try {
|
|
16003
|
+
const manifest = JSON.parse(value.manifestText);
|
|
16004
|
+
if (!record5(manifest) || manifest.version !== value.version || validatePortableManifestContract(manifest, { strict: true }).length || !hash(manifest.provenance?.content_hash))
|
|
16005
|
+
return bad();
|
|
16006
|
+
} catch {
|
|
16007
|
+
return bad();
|
|
16008
|
+
}
|
|
16009
|
+
return Object.freeze({ ...value });
|
|
16010
|
+
}
|
|
16011
|
+
function checkedPublicationView(value, skillId, intentId, declaration) {
|
|
16012
|
+
if (!exact(value, ["id", "skillId", "version", "expectedCurrentVersionId", "archiveSha256", "archiveByteSize", "state", "expiresAt", "createdAt", "versionId"]) || !publicationUuid(value.id) || value.skillId !== skillId || intentId !== undefined && value.id !== intentId || typeof value.version !== "string" || !value.version || value.version.length > 128 || /[\p{Cc}\p{Cs}]/u.test(value.version) || !(value.expectedCurrentVersionId === null || publicationUuid(value.expectedCurrentVersionId)) || !hash(value.archiveSha256) || !Number.isSafeInteger(value.archiveByteSize) || Number(value.archiveByteSize) < 1 || Number(value.archiveByteSize) > PRIVATE_PUBLICATION_MAX_BYTES || typeof value.state !== "string" || !["awaiting_upload", "queued", "verifying", "needs_attention", "committed", "rejected", "cancelled", "expired"].includes(value.state) || !date(value.expiresAt) || !date(value.createdAt) || !(value.versionId === null || publicationUuid(value.versionId)) || value.state === "committed" && value.versionId === null)
|
|
16013
|
+
return invalid3();
|
|
16014
|
+
if (declaration && ["version", "expectedCurrentVersionId", "archiveSha256", "archiveByteSize"].some((k) => value[k] !== declaration[k]))
|
|
16015
|
+
return invalid3();
|
|
16016
|
+
return Object.freeze({ ...value });
|
|
16017
|
+
}
|
|
16018
|
+
async function boundedJson(url, init, token, mutation, budget = 15000, signal) {
|
|
16019
|
+
const controller = new AbortController;
|
|
16020
|
+
let reader;
|
|
16021
|
+
let response;
|
|
16022
|
+
const timeout = new PrivatePublicationError("PUBLICATION_UNCONFIRMED", "No confirmed publication result. Inspect the saved intent before retrying.", mutation);
|
|
16023
|
+
let timer;
|
|
16024
|
+
let abort;
|
|
16025
|
+
try {
|
|
16026
|
+
return await Promise.race([(async () => {
|
|
16027
|
+
if (signal?.aborted)
|
|
16028
|
+
throw timeout;
|
|
16029
|
+
response = await fetch(url, {
|
|
16030
|
+
...init,
|
|
16031
|
+
redirect: "error",
|
|
16032
|
+
credentials: "omit",
|
|
16033
|
+
signal: controller.signal,
|
|
16034
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }
|
|
16035
|
+
});
|
|
16036
|
+
const length = response.headers.get("content-length");
|
|
16037
|
+
if (length !== null && (!/^\d+$/.test(length) || Number(length) > 65536))
|
|
16038
|
+
return invalid3();
|
|
16039
|
+
reader = response.body?.getReader();
|
|
16040
|
+
const chunks = [];
|
|
16041
|
+
let size = 0;
|
|
16042
|
+
if (reader)
|
|
16043
|
+
while (true) {
|
|
16044
|
+
const part = await reader.read();
|
|
16045
|
+
if (part.done)
|
|
16046
|
+
break;
|
|
16047
|
+
size += part.value.byteLength;
|
|
16048
|
+
if (size > 65536)
|
|
16049
|
+
return invalid3();
|
|
16050
|
+
chunks.push(part.value);
|
|
16051
|
+
}
|
|
16052
|
+
if (controller.signal.aborted)
|
|
16053
|
+
throw timeout;
|
|
16054
|
+
const bytes = Buffer.concat(chunks);
|
|
16055
|
+
let body;
|
|
16056
|
+
try {
|
|
16057
|
+
body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
16058
|
+
} catch {
|
|
16059
|
+
return invalid3();
|
|
16060
|
+
}
|
|
16061
|
+
if (!response.ok) {
|
|
16062
|
+
const code = record5(body) && typeof body.code === "string" && Object.hasOwn(failures, body.code) ? body.code : null;
|
|
16063
|
+
if (code && failures[code][0] === response.status)
|
|
16064
|
+
throw new PrivatePublicationError(code, failures[code][1], mutation && code === "PUBLICATION_UNCERTAIN", response.status);
|
|
16065
|
+
throw new PrivatePublicationError("PUBLICATION_REQUEST_FAILED", "The publication request was refused. Inspect its status before retrying.", mutation && response.status >= 500, response.status);
|
|
16066
|
+
}
|
|
16067
|
+
return body;
|
|
16068
|
+
})(), new Promise((_, reject) => {
|
|
16069
|
+
abort = () => {
|
|
16070
|
+
controller.abort();
|
|
16071
|
+
reject(timeout);
|
|
16072
|
+
};
|
|
16073
|
+
timer = setTimeout(abort, Math.max(1, Math.min(15000, budget)));
|
|
16074
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
16075
|
+
})]);
|
|
16076
|
+
} catch (error) {
|
|
16077
|
+
if (error instanceof PrivatePublicationError) {
|
|
16078
|
+
if (mutation && error.code === "INVALID_PUBLICATION_RESPONSE")
|
|
16079
|
+
throw timeout;
|
|
16080
|
+
throw error;
|
|
16081
|
+
}
|
|
16082
|
+
throw timeout;
|
|
16083
|
+
} finally {
|
|
16084
|
+
if (timer)
|
|
16085
|
+
clearTimeout(timer);
|
|
16086
|
+
if (abort)
|
|
16087
|
+
signal?.removeEventListener("abort", abort);
|
|
16088
|
+
controller.abort();
|
|
16089
|
+
if (reader)
|
|
16090
|
+
reader.cancel().catch(() => {});
|
|
16091
|
+
else
|
|
16092
|
+
response?.body?.cancel().catch(() => {});
|
|
16093
|
+
}
|
|
16094
|
+
}
|
|
16095
|
+
|
|
16096
|
+
class RemotePrivatePublicationsClient {
|
|
16097
|
+
apiOrigin;
|
|
16098
|
+
organizationId;
|
|
16099
|
+
userId;
|
|
16100
|
+
membershipId;
|
|
16101
|
+
#token;
|
|
16102
|
+
constructor(apiUrl, session) {
|
|
16103
|
+
const checked = parseWorkspaceSession(session, { userId: session?.user?.id, membershipId: session?.user?.membershipId });
|
|
16104
|
+
if (checked.user.role === "viewer")
|
|
16105
|
+
throw new PrivatePublicationError("PUBLICATION_FORBIDDEN", failures.PUBLICATION_FORBIDDEN[1]);
|
|
16106
|
+
this.apiOrigin = normalizeSkillsApiOrigin(apiUrl);
|
|
16107
|
+
this.#token = checked.token;
|
|
16108
|
+
this.organizationId = checked.organization.id;
|
|
16109
|
+
this.userId = checked.user.id;
|
|
16110
|
+
this.membershipId = checked.user.membershipId;
|
|
16111
|
+
Object.freeze(this);
|
|
16112
|
+
}
|
|
16113
|
+
async getCapability(options = {}) {
|
|
16114
|
+
if (options.timeoutMs !== undefined && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1 || options.timeoutMs > 15000))
|
|
16115
|
+
return bad();
|
|
16116
|
+
const response = await boundedJson(skillsApiRequestUrl(this.apiOrigin, "/api/v1/capabilities"), {}, this.#token, false, options.timeoutMs, options.signal);
|
|
16117
|
+
const p = record5(response) && response.privatePublishing;
|
|
16118
|
+
if (!record5(response) || response.contractVersion !== 1 || response.apiVersion !== 1 || !exact(p, ["contractVersion", "enabled", "authentication", "maxArchiveBytes", "uploadMaxTtlSeconds", "executionEnabled"]) || p.contractVersion !== 1 || typeof p.enabled !== "boolean" || p.authentication !== "interactive-session" || p.maxArchiveBytes !== PRIVATE_PUBLICATION_MAX_BYTES || p.uploadMaxTtlSeconds !== 300 || p.executionEnabled !== false)
|
|
16119
|
+
throw new PrivatePublicationError("PUBLICATION_CONTRACT_UNAVAILABLE", "This server does not support the hosted private publication contract.");
|
|
16120
|
+
return Object.freeze({ ...p });
|
|
16121
|
+
}
|
|
16122
|
+
async#gate(enabled, options = {}) {
|
|
16123
|
+
if (!(await this.getCapability(options)).enabled && enabled)
|
|
16124
|
+
throw new PrivatePublicationError("PUBLICATION_CAPABILITY_UNAVAILABLE", failures.PUBLICATION_CAPABILITY_UNAVAILABLE[1]);
|
|
16125
|
+
}
|
|
16126
|
+
#path(skillId, intentId) {
|
|
16127
|
+
if (!publicationUuid(skillId) || intentId !== undefined && !publicationUuid(intentId))
|
|
16128
|
+
return bad();
|
|
16129
|
+
return skillsApiRequestUrl(this.apiOrigin, `/api/v1/skills/${skillId}/publication-uploads${intentId ? `/${intentId}` : ""}`);
|
|
16130
|
+
}
|
|
16131
|
+
async#view(path, method, skillId, intentId, declaration, options = {}) {
|
|
16132
|
+
const value = await boundedJson(path, { method, ...method === "GET" ? {} : { body: JSON.stringify(declaration ?? {}) } }, this.#token, method !== "GET", options.timeoutMs, options.signal);
|
|
16133
|
+
try {
|
|
16134
|
+
if (!record5(value) || !Object.keys(value).every((k) => k === "upload" || k === "changed") || value.changed !== undefined && typeof value.changed !== "boolean")
|
|
16135
|
+
return invalid3();
|
|
16136
|
+
return checkedPublicationView(value.upload, skillId, intentId, declaration);
|
|
16137
|
+
} catch (error) {
|
|
16138
|
+
if (method !== "GET")
|
|
16139
|
+
throw new PrivatePublicationError("PUBLICATION_UNCONFIRMED", "The publication result could not be confirmed. Reconcile the saved intent before another action.", true);
|
|
16140
|
+
throw error;
|
|
16141
|
+
}
|
|
16142
|
+
}
|
|
16143
|
+
async begin(skillId, input) {
|
|
16144
|
+
const path = this.#path(skillId), declaration = checkedPublicationDeclaration(input);
|
|
16145
|
+
await this.#gate(true);
|
|
16146
|
+
return this.#view(path, "POST", skillId, undefined, declaration);
|
|
16147
|
+
}
|
|
16148
|
+
async get(skillId, intentId, options = {}) {
|
|
16149
|
+
const path = this.#path(skillId, intentId), until = Date.now() + (options.timeoutMs ?? 15000);
|
|
16150
|
+
await this.#gate(false, options);
|
|
16151
|
+
return this.#view(path, "GET", skillId, intentId, undefined, { ...options, timeoutMs: Math.max(1, until - Date.now()) });
|
|
16152
|
+
}
|
|
16153
|
+
async finalize(skillId, intentId) {
|
|
16154
|
+
const path = this.#path(skillId, intentId);
|
|
16155
|
+
await this.#gate(true);
|
|
16156
|
+
return this.#view(`${path}/finalize`, "POST", skillId, intentId);
|
|
16157
|
+
}
|
|
16158
|
+
async cancel(skillId, intentId) {
|
|
16159
|
+
const path = this.#path(skillId, intentId);
|
|
16160
|
+
await this.#gate(false);
|
|
16161
|
+
return this.#view(path, "DELETE", skillId, intentId);
|
|
16162
|
+
}
|
|
16163
|
+
async upload(skillId, intent, bytes) {
|
|
16164
|
+
const captured = checkedPublicationView(intent, skillId), path = this.#path(skillId, captured.id);
|
|
16165
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength !== captured.archiveByteSize)
|
|
16166
|
+
return bad();
|
|
16167
|
+
const owned = Buffer.from(bytes);
|
|
16168
|
+
if (captured.state !== "awaiting_upload" || owned.byteLength !== captured.archiveByteSize || publicationSha256(owned) !== captured.archiveSha256)
|
|
16169
|
+
return bad();
|
|
16170
|
+
await this.#gate(true);
|
|
16171
|
+
const value = await boundedJson(`${path}/upload-url`, { method: "POST", body: "{}" }, this.#token, false);
|
|
16172
|
+
const upload = this.#upload(value, captured);
|
|
16173
|
+
const controller = new AbortController;
|
|
16174
|
+
let timer;
|
|
16175
|
+
const uncertain = () => new PrivatePublicationError("PUBLICATION_UPLOAD_UNCONFIRMED", "Upload acceptance is uncertain. Resume this saved intent to finalize and inspect it; do not upload again.", true);
|
|
16176
|
+
try {
|
|
16177
|
+
await Promise.race([(async () => {
|
|
16178
|
+
const response = await fetch(upload.uploadUrl, { method: "PUT", headers: upload.headers, body: owned, redirect: "error", credentials: "omit", signal: controller.signal });
|
|
16179
|
+
response.body?.cancel().catch(() => {});
|
|
16180
|
+
if (response.status !== 200 || controller.signal.aborted)
|
|
16181
|
+
throw uncertain();
|
|
16182
|
+
})(), new Promise((_, reject) => {
|
|
16183
|
+
timer = setTimeout(() => {
|
|
16184
|
+
controller.abort();
|
|
16185
|
+
reject(uncertain());
|
|
16186
|
+
}, 30000);
|
|
16187
|
+
})]);
|
|
16188
|
+
} catch {
|
|
16189
|
+
throw uncertain();
|
|
16190
|
+
} finally {
|
|
16191
|
+
if (timer)
|
|
16192
|
+
clearTimeout(timer);
|
|
16193
|
+
controller.abort();
|
|
16194
|
+
}
|
|
16195
|
+
}
|
|
16196
|
+
#upload(value, intent) {
|
|
16197
|
+
if (!exact(value, ["upload"]) || !exact(value.upload, ["method", "uploadUrl", "headers", "expiresAt"]))
|
|
16198
|
+
return invalid3();
|
|
16199
|
+
const p = value.upload;
|
|
16200
|
+
if (p.method !== "PUT" || typeof p.uploadUrl !== "string" || p.uploadUrl.length > 8192 || /[\x00-\x20\x7f]/.test(p.uploadUrl) || !date(p.expiresAt) || Date.parse(p.expiresAt) - Date.now() < 1000 || Date.parse(p.expiresAt) - Date.now() > 300000 || Date.parse(p.expiresAt) > Date.parse(intent.expiresAt) || !exact(p.headers, ["content-type", "content-length", "x-amz-checksum-sha256", "x-amz-expected-bucket-owner"]) || p.headers["content-type"] !== "application/gzip" || p.headers["content-length"] !== String(intent.archiveByteSize) || p.headers["x-amz-checksum-sha256"] !== Buffer.from(intent.archiveSha256, "hex").toString("base64") || typeof p.headers["x-amz-expected-bucket-owner"] !== "string" || !/^\d{12}$/.test(p.headers["x-amz-expected-bucket-owner"]))
|
|
16201
|
+
return invalid3();
|
|
16202
|
+
let url;
|
|
16203
|
+
try {
|
|
16204
|
+
url = new URL(p.uploadUrl);
|
|
16205
|
+
} catch {
|
|
16206
|
+
return invalid3();
|
|
16207
|
+
}
|
|
16208
|
+
if (url.protocol !== "https:" || url.port || url.username || url.password || url.hash || !/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]\.s3\.[a-z]{2}(?:-[a-z]+)+-[1-9]\.amazonaws\.com$/.test(url.hostname) || url.pathname !== `/private-publication-staging/${this.organizationId}/${intent.id}/bundle.tgz` || !url.search || url.searchParams.get("X-Amz-Algorithm") !== "AWS4-HMAC-SHA256" || url.searchParams.get("X-Amz-SignedHeaders") !== "content-length;content-type;host;x-amz-checksum-sha256;x-amz-expected-bucket-owner" || !/^[a-f0-9]{64}$/.test(url.searchParams.get("X-Amz-Signature") ?? ""))
|
|
16209
|
+
return invalid3();
|
|
16210
|
+
const queryKeys = ["X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Date", "X-Amz-Expires", "X-Amz-Security-Token", "X-Amz-Signature", "X-Amz-SignedHeaders"];
|
|
16211
|
+
if ([...url.searchParams.keys()].sort().join(",") !== queryKeys.sort().join(","))
|
|
16212
|
+
return invalid3();
|
|
16213
|
+
const issued = url.searchParams.get("X-Amz-Date"), ttl = url.searchParams.get("X-Amz-Expires"), credential = url.searchParams.get("X-Amz-Credential");
|
|
16214
|
+
const timestamp3 = /^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)Z$/.exec(issued);
|
|
16215
|
+
const region = url.hostname.split(".s3.")[1].split(".amazonaws.com")[0];
|
|
16216
|
+
if (!timestamp3 || !/^[1-9]\d{0,2}$/.test(ttl) || Number(ttl) > 300 || !/^[A-Z0-9]{16,128}\//.test(credential) || credential.split("/").slice(1).join("/") !== `${issued.slice(0, 8)}/${region}/s3/aws4_request` || !/^[\x21-\x7e]{1,4096}$/.test(url.searchParams.get("X-Amz-Security-Token")))
|
|
16217
|
+
return invalid3();
|
|
16218
|
+
const issuedAt = Date.parse(`${timestamp3[1]}-${timestamp3[2]}-${timestamp3[3]}T${timestamp3[4]}:${timestamp3[5]}:${timestamp3[6]}Z`);
|
|
16219
|
+
if (!Number.isFinite(issuedAt) || issuedAt > Date.now() + 1000 || issuedAt + Number(ttl) * 1000 !== Date.parse(p.expiresAt))
|
|
16220
|
+
return invalid3();
|
|
16221
|
+
return { method: "PUT", uploadUrl: url.href, expiresAt: p.expiresAt, headers: Object.freeze({ ...p.headers }) };
|
|
16222
|
+
}
|
|
16223
|
+
async wait(skillId, intentId, options = {}) {
|
|
16224
|
+
const timeout = options.timeoutMs ?? 60000;
|
|
16225
|
+
if (!Number.isSafeInteger(timeout) || timeout < 0 || timeout > 300000)
|
|
16226
|
+
return bad();
|
|
16227
|
+
const until = Date.now() + timeout;
|
|
16228
|
+
let previous;
|
|
16229
|
+
while (true) {
|
|
16230
|
+
if (options.signal?.aborted)
|
|
16231
|
+
throw new PrivatePublicationError("PUBLICATION_WAIT_ABORTED", "Stopped waiting. The server publication continues; inspect the saved intent.");
|
|
16232
|
+
let view;
|
|
16233
|
+
try {
|
|
16234
|
+
view = await this.get(skillId, intentId, { timeoutMs: timeout === 0 ? 15000 : Math.max(1, Math.min(15000, until - Date.now())), signal: options.signal });
|
|
16235
|
+
} catch (error) {
|
|
16236
|
+
if (previous && Date.now() >= until && !options.signal?.aborted)
|
|
16237
|
+
return previous;
|
|
16238
|
+
throw error;
|
|
16239
|
+
}
|
|
16240
|
+
previous = view;
|
|
16241
|
+
if (!["queued", "verifying"].includes(view.state) || Date.now() >= until)
|
|
16242
|
+
return view;
|
|
16243
|
+
await new Promise((resolve4) => {
|
|
16244
|
+
const timer = setTimeout(done, Math.min(1000, until - Date.now()));
|
|
16245
|
+
function done() {
|
|
16246
|
+
clearTimeout(timer);
|
|
16247
|
+
options.signal?.removeEventListener("abort", done);
|
|
16248
|
+
resolve4();
|
|
16249
|
+
}
|
|
16250
|
+
options.signal?.addEventListener("abort", done, { once: true });
|
|
16251
|
+
});
|
|
16252
|
+
}
|
|
16253
|
+
}
|
|
16254
|
+
}
|
|
16255
|
+
|
|
15774
16256
|
// src/lib/remote-auth.ts
|
|
15775
16257
|
var MAX_ERROR_DETAIL_LENGTH = 200;
|
|
15776
16258
|
|
|
@@ -15793,10 +16275,11 @@ class HostedApiError extends Error {
|
|
|
15793
16275
|
async function requestAuthApi(instance, path, options) {
|
|
15794
16276
|
const url = normalizeSkillsApiOrigin(instance);
|
|
15795
16277
|
const safeUrl = url;
|
|
15796
|
-
const
|
|
16278
|
+
const requestUrl = skillsApiRequestUrl(url, path);
|
|
16279
|
+
const endpoint = `${(options?.method || "GET").toUpperCase()} ${requestUrl}`;
|
|
15797
16280
|
let res;
|
|
15798
16281
|
try {
|
|
15799
|
-
res = await fetch(
|
|
16282
|
+
res = await fetch(requestUrl, {
|
|
15800
16283
|
...options,
|
|
15801
16284
|
redirect: "error",
|
|
15802
16285
|
signal: options?.signal ?? AbortSignal.timeout(15000),
|
|
@@ -15811,10 +16294,10 @@ async function requestAuthApi(instance, path, options) {
|
|
|
15811
16294
|
const text2 = await res.text();
|
|
15812
16295
|
const body = text2 ? parseJsonBody(text2) : {};
|
|
15813
16296
|
if (!res.ok) {
|
|
15814
|
-
const
|
|
15815
|
-
const detail = typeof
|
|
15816
|
-
const error = typeof
|
|
15817
|
-
const code = typeof
|
|
16297
|
+
const record6 = isRecord5(body) ? body : {};
|
|
16298
|
+
const detail = typeof record6.detail === "string" ? record6.detail : undefined;
|
|
16299
|
+
const error = typeof record6.error === "string" ? record6.error : undefined;
|
|
16300
|
+
const code = typeof record6.code === "string" ? record6.code : undefined;
|
|
15818
16301
|
throw new HostedApiError(detail || error || `${res.status} ${res.statusText}`, {
|
|
15819
16302
|
status: res.status,
|
|
15820
16303
|
code,
|
|
@@ -15848,6 +16331,10 @@ class RemoteSkillsAuthClient {
|
|
|
15848
16331
|
constructor(apiUrl) {
|
|
15849
16332
|
this.apiOrigin = normalizeSkillsApiOrigin(apiUrl);
|
|
15850
16333
|
}
|
|
16334
|
+
async openPrivatePublications(email2, code, context) {
|
|
16335
|
+
const origin = this.apiOrigin, captured = workspaceContext(context);
|
|
16336
|
+
return new RemotePrivatePublicationsClient(origin, await this.switchWorkspace(email2, code, captured));
|
|
16337
|
+
}
|
|
15851
16338
|
requestInvitationEmailChallenge(input) {
|
|
15852
16339
|
return requestInvitationEmail(this.apiOrigin, "challenge", input);
|
|
15853
16340
|
}
|
|
@@ -15895,9 +16382,10 @@ class RemoteSkillsAuthClient {
|
|
|
15895
16382
|
const apiOrigin = this.apiOrigin;
|
|
15896
16383
|
if (typeof email2 !== "string" || !email2.includes("@") || typeof code !== "string" || !/^\d{6}$/.test(code))
|
|
15897
16384
|
throw new Error("Fresh email and six-digit verification code are required to manage this account");
|
|
16385
|
+
const requestUrl = skillsApiRequestUrl(apiOrigin, "/api/auth/verify");
|
|
15898
16386
|
let response;
|
|
15899
16387
|
try {
|
|
15900
|
-
response = await fetch(
|
|
16388
|
+
response = await fetch(requestUrl, {
|
|
15901
16389
|
method: "POST",
|
|
15902
16390
|
redirect: "error",
|
|
15903
16391
|
credentials: "omit",
|
|
@@ -15988,6 +16476,242 @@ class RemoteSkillsAuthClient {
|
|
|
15988
16476
|
return requestAuthApi(this.apiOrigin, path, options);
|
|
15989
16477
|
}
|
|
15990
16478
|
}
|
|
16479
|
+
// src/lib/private-publication-recovery.ts
|
|
16480
|
+
import { constants as constants2, closeSync as closeSync3, fsyncSync, fstatSync as fstatSync3, lstatSync as lstatSync5, mkdirSync as mkdirSync14, openSync as openSync3, readSync as readSync2, realpathSync as realpathSync2, renameSync as renameSync4, unlinkSync, writeFileSync as writeFileSync13 } from "fs";
|
|
16481
|
+
import { dirname as dirname12, isAbsolute as isAbsolute5, join as join22, resolve as resolve4 } from "path";
|
|
16482
|
+
import { randomUUID } from "crypto";
|
|
16483
|
+
var fail2 = () => {
|
|
16484
|
+
throw new PrivatePublicationError("PUBLICATION_RECOVERY_INVALID", "The recovery directory is invalid or changed. Preserve it and inspect the existing intent; do not start a replacement automatically.");
|
|
16485
|
+
};
|
|
16486
|
+
function safeDirectory(directory) {
|
|
16487
|
+
if (!isAbsolute5(directory) || directory !== resolve4(directory) || realpathSync2(directory) !== directory)
|
|
16488
|
+
return fail2();
|
|
16489
|
+
for (let path = directory;; path = dirname12(path)) {
|
|
16490
|
+
const stat = lstatSync5(path);
|
|
16491
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
16492
|
+
return fail2();
|
|
16493
|
+
if (path === dirname12(path))
|
|
16494
|
+
break;
|
|
16495
|
+
}
|
|
16496
|
+
const own = lstatSync5(directory);
|
|
16497
|
+
if ((own.mode & 63) !== 0 || process.getuid && own.uid !== process.getuid())
|
|
16498
|
+
return fail2();
|
|
16499
|
+
return { dev: own.dev, ino: own.ino };
|
|
16500
|
+
}
|
|
16501
|
+
function unchangedDirectory(directory, identity) {
|
|
16502
|
+
const now = safeDirectory(directory);
|
|
16503
|
+
if (now.dev !== identity.dev || now.ino !== identity.ino)
|
|
16504
|
+
return fail2();
|
|
16505
|
+
}
|
|
16506
|
+
function readOwned(directory, name, max) {
|
|
16507
|
+
const identity = safeDirectory(directory), file = join22(directory, name);
|
|
16508
|
+
const fd = openSync3(file, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
16509
|
+
try {
|
|
16510
|
+
const stat = fstatSync3(fd);
|
|
16511
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > max || stat.size < 1 || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
|
|
16512
|
+
return fail2();
|
|
16513
|
+
const buffer = Buffer.alloc(Math.min(max, stat.size) + 1);
|
|
16514
|
+
let length = 0;
|
|
16515
|
+
while (length < buffer.length) {
|
|
16516
|
+
const count = readSync2(fd, buffer, length, buffer.length - length, length);
|
|
16517
|
+
if (count === 0)
|
|
16518
|
+
break;
|
|
16519
|
+
length += count;
|
|
16520
|
+
}
|
|
16521
|
+
const bytes = buffer.subarray(0, length), after = fstatSync3(fd);
|
|
16522
|
+
unchangedDirectory(directory, identity);
|
|
16523
|
+
if (bytes.length !== stat.size || stat.size !== after.size || stat.mtimeMs !== after.mtimeMs || stat.ctimeMs !== after.ctimeMs)
|
|
16524
|
+
return fail2();
|
|
16525
|
+
return bytes;
|
|
16526
|
+
} finally {
|
|
16527
|
+
closeSync3(fd);
|
|
16528
|
+
}
|
|
16529
|
+
}
|
|
16530
|
+
function writeOwned(directory, name, bytes) {
|
|
16531
|
+
const fd = openSync3(join22(directory, name), constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
16532
|
+
try {
|
|
16533
|
+
writeFileSync13(fd, bytes);
|
|
16534
|
+
fsyncSync(fd);
|
|
16535
|
+
} finally {
|
|
16536
|
+
closeSync3(fd);
|
|
16537
|
+
}
|
|
16538
|
+
}
|
|
16539
|
+
function save(directory, value) {
|
|
16540
|
+
const identity = safeDirectory(directory), temporary = `.receipt-${randomUUID()}.json`;
|
|
16541
|
+
writeOwned(directory, temporary, JSON.stringify(value) + `
|
|
16542
|
+
`);
|
|
16543
|
+
unchangedDirectory(directory, identity);
|
|
16544
|
+
renameSync4(join22(directory, temporary), join22(directory, "receipt.json"));
|
|
16545
|
+
const fd = openSync3(directory, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
16546
|
+
try {
|
|
16547
|
+
fsyncSync(fd);
|
|
16548
|
+
} finally {
|
|
16549
|
+
closeSync3(fd);
|
|
16550
|
+
}
|
|
16551
|
+
}
|
|
16552
|
+
function bind(client, receipt) {
|
|
16553
|
+
if (["apiOrigin", "organizationId", "userId", "membershipId"].some((k) => client[k] !== receipt[k]))
|
|
16554
|
+
throw new PrivatePublicationError("PUBLICATION_IDENTITY_CHANGED", "The fresh session does not match the recovery directory's server, account and membership.");
|
|
16555
|
+
}
|
|
16556
|
+
async function locked(directory, action) {
|
|
16557
|
+
const identity = safeDirectory(directory), file = join22(directory, "operation.lock");
|
|
16558
|
+
let fd;
|
|
16559
|
+
try {
|
|
16560
|
+
fd = openSync3(file, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
16561
|
+
} catch {
|
|
16562
|
+
throw new PrivatePublicationError("PUBLICATION_RECOVERY_BUSY", "Another operation holds this recovery directory. If it crashed, confirm that process has stopped before explicitly removing operation.lock and resuming.");
|
|
16563
|
+
}
|
|
16564
|
+
const lock = fstatSync3(fd);
|
|
16565
|
+
try {
|
|
16566
|
+
writeFileSync13(fd, JSON.stringify({ pid: process.pid }) + `
|
|
16567
|
+
`);
|
|
16568
|
+
fsyncSync(fd);
|
|
16569
|
+
return await action();
|
|
16570
|
+
} finally {
|
|
16571
|
+
closeSync3(fd);
|
|
16572
|
+
unchangedDirectory(directory, identity);
|
|
16573
|
+
const now = lstatSync5(file);
|
|
16574
|
+
if (now.dev !== lock.dev || now.ino !== lock.ino || !now.isFile() || now.isSymbolicLink())
|
|
16575
|
+
fail2();
|
|
16576
|
+
unlinkSync(file);
|
|
16577
|
+
}
|
|
16578
|
+
}
|
|
16579
|
+
function readPrivatePublicationRecovery(directory) {
|
|
16580
|
+
try {
|
|
16581
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readOwned(directory, "receipt.json", 65536)));
|
|
16582
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== ["contractVersion", "apiOrigin", "organizationId", "userId", "membershipId", "skillId", "declaration", "phase", "intent"].sort().join(",") || value.contractVersion !== 1 || ![value.organizationId, value.userId, value.membershipId, value.skillId].every(publicationUuid) || typeof value.apiOrigin !== "string" || normalizeSkillsApiOrigin(value.apiOrigin) !== value.apiOrigin || !["prepared", "begin_uncertain", "awaiting_upload", "upload_uncertain", "uploaded", "finalize_uncertain", "observed"].includes(value.phase))
|
|
16583
|
+
return fail2();
|
|
16584
|
+
value.declaration = checkedPublicationDeclaration(value.declaration);
|
|
16585
|
+
if (value.intent !== null)
|
|
16586
|
+
value.intent = checkedPublicationView(value.intent, value.skillId, undefined, value.declaration);
|
|
16587
|
+
if (value.intent === null && !["prepared", "begin_uncertain"].includes(value.phase))
|
|
16588
|
+
return fail2();
|
|
16589
|
+
const bytes = readOwned(directory, "bundle.tgz", PRIVATE_PUBLICATION_MAX_BYTES);
|
|
16590
|
+
if (bytes.length !== value.declaration.archiveByteSize || publicationSha256(bytes) !== value.declaration.archiveSha256)
|
|
16591
|
+
return fail2();
|
|
16592
|
+
return { receipt: value, bytes };
|
|
16593
|
+
} catch {
|
|
16594
|
+
return fail2();
|
|
16595
|
+
}
|
|
16596
|
+
}
|
|
16597
|
+
async function preparePrivatePublication(client, sourceDirectory, recoveryDirectory, input) {
|
|
16598
|
+
if (!publicationUuid(input.skillId) || !(input.expectedCurrentVersionId === null || publicationUuid(input.expectedCurrentVersionId)) || input.idempotencyKey !== undefined && !publicationUuid(input.idempotencyKey))
|
|
16599
|
+
return fail2();
|
|
16600
|
+
const packed = packSkillBundle(sourceDirectory, { maxUnpackedBytes: 32 * 1024 * 1024 });
|
|
16601
|
+
const inspected = await inspectSkillBundle(packed.bytes, { limits: { compressedBytes: PRIVATE_PUBLICATION_MAX_BYTES } });
|
|
16602
|
+
const manifest = inspected.entries.find((entry) => entry.path === "skill.json");
|
|
16603
|
+
if (!manifest || manifest.bytes.length > 16384 || !(await verifyContentHashFromEntries(inspected.entries)).valid)
|
|
16604
|
+
throw new PrivatePublicationError("PUBLICATION_BUNDLE_INVALID", "The skill must have a valid skill.json with its current content hash. Validate the skill before publishing.");
|
|
16605
|
+
const manifestText = new TextDecoder("utf-8", { fatal: true }).decode(manifest.bytes);
|
|
16606
|
+
const declaration = checkedPublicationDeclaration({
|
|
16607
|
+
idempotencyKey: input.idempotencyKey ?? randomUUID(),
|
|
16608
|
+
version: JSON.parse(manifestText).version,
|
|
16609
|
+
expectedCurrentVersionId: input.expectedCurrentVersionId,
|
|
16610
|
+
manifestText,
|
|
16611
|
+
archiveSha256: inspected.sha256,
|
|
16612
|
+
archiveByteSize: packed.bytes.length
|
|
16613
|
+
});
|
|
16614
|
+
const receipt = {
|
|
16615
|
+
contractVersion: 1,
|
|
16616
|
+
apiOrigin: client.apiOrigin,
|
|
16617
|
+
organizationId: client.organizationId,
|
|
16618
|
+
userId: client.userId,
|
|
16619
|
+
membershipId: client.membershipId,
|
|
16620
|
+
skillId: input.skillId,
|
|
16621
|
+
declaration,
|
|
16622
|
+
phase: "prepared",
|
|
16623
|
+
intent: null
|
|
16624
|
+
};
|
|
16625
|
+
if (!isAbsolute5(recoveryDirectory) || resolve4(recoveryDirectory) !== recoveryDirectory || realpathSync2(dirname12(recoveryDirectory)) !== dirname12(recoveryDirectory))
|
|
16626
|
+
return fail2();
|
|
16627
|
+
mkdirSync14(recoveryDirectory, { mode: 448 });
|
|
16628
|
+
safeDirectory(recoveryDirectory);
|
|
16629
|
+
const parent = openSync3(dirname12(recoveryDirectory), constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
16630
|
+
try {
|
|
16631
|
+
fsyncSync(parent);
|
|
16632
|
+
} finally {
|
|
16633
|
+
closeSync3(parent);
|
|
16634
|
+
}
|
|
16635
|
+
writeOwned(recoveryDirectory, "bundle.tgz", packed.bytes);
|
|
16636
|
+
save(recoveryDirectory, receipt);
|
|
16637
|
+
return receipt;
|
|
16638
|
+
}
|
|
16639
|
+
function privatePublicationResult(directory, receipt) {
|
|
16640
|
+
const state = receipt.intent?.state ?? receipt.phase, committed = state === "committed";
|
|
16641
|
+
const nextAction = committed ? "The version is published. Private execution remains unavailable." : ["rejected", "cancelled", "expired"].includes(state) ? "This intent is terminal. Inspect the result before explicitly preparing another version." : state === "needs_attention" ? "Keep this intent and contact the service operator; do not create a replacement or upload again." : receipt.phase === "upload_uncertain" ? "Run publication resume with this recovery directory to finalize the same intent without another upload." : receipt.intent ? "Run publication status or resume with this recovery directory; cancel explicitly if you want to stop." : "Run publication resume with this recovery directory to reconcile the identical request key and declaration.";
|
|
16642
|
+
return {
|
|
16643
|
+
recoveryDirectory: directory,
|
|
16644
|
+
skillId: receipt.skillId,
|
|
16645
|
+
intentId: receipt.intent?.id ?? null,
|
|
16646
|
+
state,
|
|
16647
|
+
versionId: receipt.intent?.versionId ?? null,
|
|
16648
|
+
committed,
|
|
16649
|
+
executionEnabled: false,
|
|
16650
|
+
nextAction
|
|
16651
|
+
};
|
|
16652
|
+
}
|
|
16653
|
+
async function continuePrivatePublication(client, directory, options) {
|
|
16654
|
+
if (options.confirm !== true)
|
|
16655
|
+
throw new PrivatePublicationError("PUBLICATION_CONFIRM_REQUIRED", "Explicit upload confirmation is required.");
|
|
16656
|
+
if (options.waitMs !== undefined && (!Number.isSafeInteger(options.waitMs) || options.waitMs < 0 || options.waitMs > 300000))
|
|
16657
|
+
return fail2();
|
|
16658
|
+
return locked(directory, () => continueLocked(client, directory, options));
|
|
16659
|
+
}
|
|
16660
|
+
async function continueLocked(client, directory, options) {
|
|
16661
|
+
const { receipt, bytes } = readPrivatePublicationRecovery(directory);
|
|
16662
|
+
bind(client, receipt);
|
|
16663
|
+
if (!receipt.intent) {
|
|
16664
|
+
receipt.phase = "begin_uncertain";
|
|
16665
|
+
save(directory, receipt);
|
|
16666
|
+
receipt.intent = await client.begin(receipt.skillId, receipt.declaration);
|
|
16667
|
+
receipt.phase = "awaiting_upload";
|
|
16668
|
+
save(directory, receipt);
|
|
16669
|
+
} else {
|
|
16670
|
+
receipt.intent = checkedPublicationView(await client.get(receipt.skillId, receipt.intent.id), receipt.skillId, receipt.intent.id, receipt.declaration);
|
|
16671
|
+
save(directory, receipt);
|
|
16672
|
+
}
|
|
16673
|
+
if (receipt.intent.state === "awaiting_upload") {
|
|
16674
|
+
if (receipt.phase === "awaiting_upload") {
|
|
16675
|
+
receipt.phase = "upload_uncertain";
|
|
16676
|
+
save(directory, receipt);
|
|
16677
|
+
try {
|
|
16678
|
+
await client.upload(receipt.skillId, receipt.intent, bytes);
|
|
16679
|
+
} catch (error) {
|
|
16680
|
+
if (error instanceof PrivatePublicationError && !error.uncertain) {
|
|
16681
|
+
receipt.phase = "awaiting_upload";
|
|
16682
|
+
save(directory, receipt);
|
|
16683
|
+
}
|
|
16684
|
+
throw error;
|
|
16685
|
+
}
|
|
16686
|
+
receipt.phase = "uploaded";
|
|
16687
|
+
save(directory, receipt);
|
|
16688
|
+
}
|
|
16689
|
+
receipt.phase = "finalize_uncertain";
|
|
16690
|
+
save(directory, receipt);
|
|
16691
|
+
receipt.intent = checkedPublicationView(await client.finalize(receipt.skillId, receipt.intent.id), receipt.skillId, receipt.intent.id, receipt.declaration);
|
|
16692
|
+
receipt.phase = "observed";
|
|
16693
|
+
save(directory, receipt);
|
|
16694
|
+
}
|
|
16695
|
+
if (options.waitMs !== undefined && options.waitMs > 0 && ["queued", "verifying"].includes(receipt.intent.state)) {
|
|
16696
|
+
receipt.intent = checkedPublicationView(await client.wait(receipt.skillId, receipt.intent.id, { timeoutMs: options.waitMs }), receipt.skillId, receipt.intent.id, receipt.declaration);
|
|
16697
|
+
receipt.phase = "observed";
|
|
16698
|
+
save(directory, receipt);
|
|
16699
|
+
}
|
|
16700
|
+
return privatePublicationResult(directory, receipt);
|
|
16701
|
+
}
|
|
16702
|
+
async function inspectPrivatePublication(client, directory, cancel = false) {
|
|
16703
|
+
return locked(directory, () => inspectLocked(client, directory, cancel));
|
|
16704
|
+
}
|
|
16705
|
+
async function inspectLocked(client, directory, cancel) {
|
|
16706
|
+
const { receipt } = readPrivatePublicationRecovery(directory);
|
|
16707
|
+
bind(client, receipt);
|
|
16708
|
+
if (receipt.intent) {
|
|
16709
|
+
receipt.intent = checkedPublicationView(await (cancel ? client.cancel(receipt.skillId, receipt.intent.id) : client.get(receipt.skillId, receipt.intent.id)), receipt.skillId, receipt.intent.id, receipt.declaration);
|
|
16710
|
+
save(directory, receipt);
|
|
16711
|
+
} else if (cancel)
|
|
16712
|
+
throw new PrivatePublicationError("PUBLICATION_INTENT_UNKNOWN", "Reconcile the saved begin request with publication resume before cancelling its intent.");
|
|
16713
|
+
return privatePublicationResult(directory, receipt);
|
|
16714
|
+
}
|
|
15991
16715
|
export {
|
|
15992
16716
|
writeStationSnapshot,
|
|
15993
16717
|
writeStationHydration,
|
|
@@ -16050,11 +16774,13 @@ export {
|
|
|
16050
16774
|
removeSchedule,
|
|
16051
16775
|
removeManagedAgentSkill,
|
|
16052
16776
|
recordScheduleRun,
|
|
16777
|
+
readPrivatePublicationRecovery,
|
|
16053
16778
|
readPortableSkillManifest,
|
|
16054
16779
|
pullSkills,
|
|
16055
16780
|
publicDiscoveryEnvVars,
|
|
16056
16781
|
publicDiscoveryDocumentation,
|
|
16057
16782
|
publicDiscoveryDependencies,
|
|
16783
|
+
preparePrivatePublication,
|
|
16058
16784
|
portPortableSkillDirectory,
|
|
16059
16785
|
portPortableSkill,
|
|
16060
16786
|
pointerSkillMd,
|
|
@@ -16098,6 +16824,7 @@ export {
|
|
|
16098
16824
|
installSkillManifest,
|
|
16099
16825
|
installSkillForAgent,
|
|
16100
16826
|
installSkill,
|
|
16827
|
+
inspectPrivatePublication,
|
|
16101
16828
|
importSkillsLocalSnapshot,
|
|
16102
16829
|
homePathFor,
|
|
16103
16830
|
getToolPrimitive,
|
|
@@ -16161,6 +16888,7 @@ export {
|
|
|
16161
16888
|
createRegistrySyncArtifact,
|
|
16162
16889
|
createMcpContractManifest,
|
|
16163
16890
|
createLocalSkillManifest,
|
|
16891
|
+
continuePrivatePublication,
|
|
16164
16892
|
configuredSkillsApiUrl,
|
|
16165
16893
|
computeContentHashFromEntries,
|
|
16166
16894
|
computeContentHash,
|
|
@@ -16219,6 +16947,8 @@ export {
|
|
|
16219
16947
|
RemoteSkillsAuthClient,
|
|
16220
16948
|
RemoteRouteUnsupportedError,
|
|
16221
16949
|
RemoteRequestError,
|
|
16950
|
+
RemoteQuoteUnavailableError,
|
|
16951
|
+
RemotePrivatePublicationsClient,
|
|
16222
16952
|
RemoteInvitationEmailUnconfirmedError,
|
|
16223
16953
|
RemoteInvitationEmailError,
|
|
16224
16954
|
RemoteCreditApprovalError,
|
|
@@ -16226,7 +16956,9 @@ export {
|
|
|
16226
16956
|
REMOTE_SKILL_RUN_CONTRACT_VERSION,
|
|
16227
16957
|
REFUSED_SCANNER_FLAGGED,
|
|
16228
16958
|
PullSkillError,
|
|
16959
|
+
PrivatePublicationError,
|
|
16229
16960
|
PROJECT_CONFIG_FILE,
|
|
16961
|
+
PRIVATE_PUBLICATION_MAX_BYTES,
|
|
16230
16962
|
PORTABLE_SKILL_STANDARD,
|
|
16231
16963
|
PORTABLE_SKILL_SCHEMA,
|
|
16232
16964
|
PORTABLE_SKILL_DEFAULT_VERSION,
|