@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/sdk/index.js
CHANGED
|
@@ -23115,6 +23115,7 @@ import { spawn } from "child_process";
|
|
|
23115
23115
|
import { request as nodeHttpRequest } from "http";
|
|
23116
23116
|
import { request as nodeHttpsRequest } from "https";
|
|
23117
23117
|
import { randomUUID as randomUUID22 } from "crypto";
|
|
23118
|
+
import { createRequire as createRequire2 } from "module";
|
|
23118
23119
|
function getPathValue(input, path) {
|
|
23119
23120
|
return path.split(".").reduce((value, part) => {
|
|
23120
23121
|
if (value && typeof value === "object" && part in value) {
|
|
@@ -24330,7 +24331,7 @@ function normalizeRetryPolicy(policy) {
|
|
|
24330
24331
|
}
|
|
24331
24332
|
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE2, HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", EVENTS_STORE_SENTINEL_FILE = "events.json", LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:", DEFAULT_EVENT_PAGE_LIMIT = 100, MAX_EVENT_PAGE_LIMIT = 1000, DEFAULT_SIGNATURE_TOLERANCE_MS, DEFAULT_MAX_REDIRECTS = 5, IPV4_PRIVATE_RANGES, IPV6_SPECIAL_PREFIXES, defaultTargetLookup = async (hostname) => {
|
|
24332
24333
|
return dnsLookup(hostname, { all: true, verbatim: false });
|
|
24333
|
-
}, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES;
|
|
24334
|
+
}, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES, MAX_CREDENTIAL_FILE_BYTES2, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER2, requireSecretsSdk2, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2, MAX_ENVELOPE_BYTES, MAX_REQUEST_BYTES;
|
|
24334
24335
|
var init_dist = __esm(() => {
|
|
24335
24336
|
PATHS_RESOLVER_KIND_ENV = {
|
|
24336
24337
|
config: "HASNA_CONFIG_HOME",
|
|
@@ -24386,6 +24387,22 @@ var init_dist = __esm(() => {
|
|
|
24386
24387
|
};
|
|
24387
24388
|
defaultEventTypeCatalog = new EventTypeCatalog;
|
|
24388
24389
|
APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
|
|
24390
|
+
MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
|
|
24391
|
+
INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
|
|
24392
|
+
CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
|
|
24393
|
+
AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
24394
|
+
SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
|
|
24395
|
+
requireSecretsSdk2 = createRequire2(import.meta.url);
|
|
24396
|
+
IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
24397
|
+
AUTHORITY_OVERRIDE_HEADERS2 = new Set([
|
|
24398
|
+
"host",
|
|
24399
|
+
":authority",
|
|
24400
|
+
"forwarded",
|
|
24401
|
+
"x-forwarded-host",
|
|
24402
|
+
"x-original-host"
|
|
24403
|
+
]);
|
|
24404
|
+
MAX_ENVELOPE_BYTES = 256 * 1024;
|
|
24405
|
+
MAX_REQUEST_BYTES = MAX_ENVELOPE_BYTES * 2 + 8192;
|
|
24389
24406
|
});
|
|
24390
24407
|
|
|
24391
24408
|
// src/sdk/event-sink.ts
|
|
@@ -25237,7 +25254,9 @@ function normalizeSkillsApiOrigin(apiUrl) {
|
|
|
25237
25254
|
throw new SkillsFleetCredentialError("A Skills API URL must use HTTPS (or loopback HTTP), without credentials, query or fragment", "INVALID_API_URL");
|
|
25238
25255
|
}
|
|
25239
25256
|
const pathname = url.pathname.replace(/\/+$/, "");
|
|
25240
|
-
if (
|
|
25257
|
+
if (url.origin === "https://api.hasna.com" && pathname === "/skills/v1") {
|
|
25258
|
+
url.pathname = "/skills";
|
|
25259
|
+
} else if (pathname === "/api" || pathname === "/api/v1") {
|
|
25241
25260
|
url.pathname = "/";
|
|
25242
25261
|
} else if (pathname.endsWith("/api/v1")) {
|
|
25243
25262
|
url.pathname = pathname.slice(0, -"/api/v1".length) || "/";
|
|
@@ -25246,6 +25265,21 @@ function normalizeSkillsApiOrigin(apiUrl) {
|
|
|
25246
25265
|
}
|
|
25247
25266
|
return url.toString().replace(/\/+$/, "");
|
|
25248
25267
|
}
|
|
25268
|
+
function skillsApiRequestUrl(apiUrl, route) {
|
|
25269
|
+
const origin = normalizeSkillsApiOrigin(apiUrl);
|
|
25270
|
+
if (!route.startsWith("/api/") || route.includes("#") || route.includes("\\")) {
|
|
25271
|
+
throw new SkillsFleetCredentialError("Invalid Skills API route", "INVALID_API_URL");
|
|
25272
|
+
}
|
|
25273
|
+
if (origin === "https://api.hasna.com/skills") {
|
|
25274
|
+
if (route === "/api/auth/whoami")
|
|
25275
|
+
return `${origin}/v1/auth/whoami`;
|
|
25276
|
+
if (!route.startsWith("/api/v1/")) {
|
|
25277
|
+
throw new SkillsFleetCredentialError("The internal Skills gateway has no established login contract yet. Select an explicitly configured instance with supported authentication.", "GATEWAY_AUTH_UNAVAILABLE");
|
|
25278
|
+
}
|
|
25279
|
+
return `${origin}${route.slice("/api".length)}`;
|
|
25280
|
+
}
|
|
25281
|
+
return `${origin}${route}`;
|
|
25282
|
+
}
|
|
25249
25283
|
function configuredSkillsApiUrl(env = process.env, keychain, profile) {
|
|
25250
25284
|
const declared = SKILLS_API_URL_ENV_KEYS.filter((key) => env[key] !== undefined).map((key) => ({ key, value: env[key] }));
|
|
25251
25285
|
for (const entry of declared) {
|
|
@@ -25462,7 +25496,7 @@ class MissingSkillsFleetError extends Error {
|
|
|
25462
25496
|
// package.json
|
|
25463
25497
|
var package_default = {
|
|
25464
25498
|
name: "@hasna/skills",
|
|
25465
|
-
version: "0.5.
|
|
25499
|
+
version: "0.5.5",
|
|
25466
25500
|
description: "Skills library for AI coding agents",
|
|
25467
25501
|
type: "module",
|
|
25468
25502
|
bin: {
|
|
@@ -37097,10 +37131,16 @@ class SqliteSkillsStore {
|
|
|
37097
37131
|
const orgId = input.principal.orgId;
|
|
37098
37132
|
const now = nowIso();
|
|
37099
37133
|
return this.db.transaction(() => {
|
|
37100
|
-
const previous = this.get("SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
37134
|
+
const previous = this.get("SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at, source FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
37101
37135
|
const previousSha = typeof previous?.bundle_sha256 === "string" ? previous.bundle_sha256 : null;
|
|
37102
37136
|
const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? previous.revision_id : null;
|
|
37103
37137
|
const tombstoned = previous?.tombstoned_at != null;
|
|
37138
|
+
if (input.seedBundledOnly && input.expectedRevisionId && !previous) {
|
|
37139
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, null);
|
|
37140
|
+
}
|
|
37141
|
+
if (input.seedBundledOnly && previous && (tombstoned || previous.source !== "bundled")) {
|
|
37142
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
37143
|
+
}
|
|
37104
37144
|
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? previous.skill_md : null;
|
|
37105
37145
|
if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
|
|
37106
37146
|
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
@@ -37174,8 +37214,9 @@ class SqliteSkillsStore {
|
|
|
37174
37214
|
tombstoned_at = NULL,
|
|
37175
37215
|
tombstone_purge_after = NULL,
|
|
37176
37216
|
updated_at = excluded.updated_at
|
|
37177
|
-
WHERE skills_registry.tombstoned_at IS NOT NULL
|
|
37178
|
-
|
|
37217
|
+
WHERE (skills_registry.tombstoned_at IS NOT NULL OR skills_registry.revision_id = ?)
|
|
37218
|
+
AND (? = 0 OR (skills_registry.tombstoned_at IS NULL AND skills_registry.source = 'bundled'
|
|
37219
|
+
AND skills_registry.revision_id = ?))
|
|
37179
37220
|
RETURNING *`, [
|
|
37180
37221
|
orgId,
|
|
37181
37222
|
input.slug,
|
|
@@ -37193,6 +37234,8 @@ class SqliteSkillsStore {
|
|
|
37193
37234
|
revisionId,
|
|
37194
37235
|
now,
|
|
37195
37236
|
now,
|
|
37237
|
+
input.expectedRevisionId ?? NO_REVISION_SENTINEL,
|
|
37238
|
+
input.seedBundledOnly ? 1 : 0,
|
|
37196
37239
|
input.expectedRevisionId ?? NO_REVISION_SENTINEL
|
|
37197
37240
|
]);
|
|
37198
37241
|
if (!row) {
|
|
@@ -42238,6 +42281,12 @@ async function storePublishedSkill(store, artifactStorage, principal, parsed, ex
|
|
|
42238
42281
|
principal,
|
|
42239
42282
|
...expectedRevisionId ? { expectedRevisionId } : {}
|
|
42240
42283
|
};
|
|
42284
|
+
if (input.seedBundledOnly && expectedRevisionId && !current) {
|
|
42285
|
+
throw new SkillRevisionConflictError(input.slug, expectedRevisionId, null);
|
|
42286
|
+
}
|
|
42287
|
+
if (input.seedBundledOnly && current && (current.tombstonedAt || current.source !== "bundled")) {
|
|
42288
|
+
throw new SkillRevisionConflictError(input.slug, expectedRevisionId, current.revisionId);
|
|
42289
|
+
}
|
|
42241
42290
|
if (current && !current.tombstonedAt && expectedRevisionId !== current.revisionId) {
|
|
42242
42291
|
throw new SkillRevisionConflictError(input.slug, expectedRevisionId, current.revisionId);
|
|
42243
42292
|
}
|
|
@@ -42487,6 +42536,15 @@ async function seedBundledCorpus(options) {
|
|
|
42487
42536
|
result.skipped.push(slug);
|
|
42488
42537
|
continue;
|
|
42489
42538
|
}
|
|
42539
|
+
const current = await options.store.getSkill(options.principal, slug);
|
|
42540
|
+
if (current) {
|
|
42541
|
+
const previousVersion = current.version ? await options.store.getSkillVersion(options.principal, slug, current.version) : null;
|
|
42542
|
+
const provenance = previousVersion?.manifest.provenance;
|
|
42543
|
+
if (current.tombstonedAt || current.source !== "bundled" || provenance?.seededFrom !== "bundled-corpus" || provenance.seededRevisionId !== current.revisionId) {
|
|
42544
|
+
result.skipped.push(slug);
|
|
42545
|
+
continue;
|
|
42546
|
+
}
|
|
42547
|
+
}
|
|
42490
42548
|
const dir = getSkillPath(slug);
|
|
42491
42549
|
if (!existsSync15(dir)) {
|
|
42492
42550
|
result.skipped.push(slug);
|
|
@@ -42496,32 +42554,40 @@ async function seedBundledCorpus(options) {
|
|
|
42496
42554
|
const packed = packSkillBundle(dir, { maxUnpackedBytes: 50000000 });
|
|
42497
42555
|
const skillMdPath = join19(dir, "SKILL.md");
|
|
42498
42556
|
const skillMd = existsSync15(skillMdPath) ? readFileSync16(skillMdPath, "utf-8") : undefined;
|
|
42499
|
-
const
|
|
42557
|
+
const content = {
|
|
42558
|
+
slug,
|
|
42559
|
+
displayName: manifest.displayName ?? skill.displayName ?? slug,
|
|
42560
|
+
description: manifest.description ?? skill.description ?? slug,
|
|
42561
|
+
category: manifest.category ?? skill.category ?? "Development Tools",
|
|
42562
|
+
tags: manifest.tags ?? skill.tags ?? [],
|
|
42563
|
+
source: "bundled",
|
|
42564
|
+
kind: manifest.kind ?? "instruction",
|
|
42565
|
+
version: version2,
|
|
42566
|
+
...skillMd ? { skillMd } : {},
|
|
42567
|
+
bundleSha256: packed.sha256,
|
|
42568
|
+
bundleByteSize: packed.bytes.byteLength
|
|
42569
|
+
};
|
|
42500
42570
|
const { record } = await storePublishedSkill(options.store, options.artifactStorage, options.principal, {
|
|
42501
42571
|
input: {
|
|
42502
|
-
|
|
42503
|
-
|
|
42504
|
-
description: manifest.description ?? skill.description ?? slug,
|
|
42505
|
-
category: manifest.category ?? skill.category ?? "Development Tools",
|
|
42506
|
-
tags: manifest.tags ?? skill.tags ?? [],
|
|
42507
|
-
source: "bundled",
|
|
42508
|
-
kind: manifest.kind ?? "instruction",
|
|
42509
|
-
version: version2,
|
|
42510
|
-
...skillMd ? { skillMd } : {},
|
|
42572
|
+
...content,
|
|
42573
|
+
seedBundledOnly: true,
|
|
42511
42574
|
bundle: { sha256: packed.sha256, byteSize: packed.bytes.byteLength, contentType: "application/gzip", storageKind: "db" },
|
|
42512
42575
|
versionManifest: {
|
|
42513
42576
|
files: packed.paths,
|
|
42514
42577
|
fileCount: packed.fileCount,
|
|
42515
42578
|
unpackedByteSize: packed.unpackedByteSize,
|
|
42516
42579
|
bundleSha256: packed.sha256,
|
|
42517
|
-
provenance: { seededFrom: "bundled-corpus", packageVersion: package_default.version }
|
|
42580
|
+
provenance: { seededFrom: "bundled-corpus", packageVersion: package_default.version, seededRevisionId: revisionIdOfRecord(content) }
|
|
42518
42581
|
}
|
|
42519
42582
|
},
|
|
42520
42583
|
bundleBytes: packed.bytes
|
|
42521
42584
|
}, current?.revisionId);
|
|
42522
42585
|
result.seeded.push(`${record.slug}@${version2}`);
|
|
42523
42586
|
} catch (error) {
|
|
42524
|
-
|
|
42587
|
+
if (error instanceof SkillRevisionConflictError)
|
|
42588
|
+
result.skipped.push(slug);
|
|
42589
|
+
else
|
|
42590
|
+
result.failed.push({ slug, error: error.message });
|
|
42525
42591
|
}
|
|
42526
42592
|
}
|
|
42527
42593
|
log(`skills: bundled corpus seed v${version2}: ${result.seeded.length} seeded, ${result.skipped.length} skipped, ${result.failed.length} failed`);
|
|
@@ -42731,6 +42797,12 @@ class MemorySkillsStore {
|
|
|
42731
42797
|
const key = skillKey(input.principal.orgId, input.slug);
|
|
42732
42798
|
const now = nowIso();
|
|
42733
42799
|
const previous = this.skills.get(key);
|
|
42800
|
+
if (input.seedBundledOnly && input.expectedRevisionId && !previous) {
|
|
42801
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, null);
|
|
42802
|
+
}
|
|
42803
|
+
if (input.seedBundledOnly && previous && (previous.tombstonedAt || previous.source !== "bundled")) {
|
|
42804
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previous.revisionId);
|
|
42805
|
+
}
|
|
42734
42806
|
if (previous && !previous.tombstonedAt && input.expectedRevisionId !== previous.revisionId) {
|
|
42735
42807
|
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previous.revisionId);
|
|
42736
42808
|
}
|
|
@@ -43184,13 +43256,19 @@ class PostgresSkillsStore {
|
|
|
43184
43256
|
const orgId = input.principal.orgId;
|
|
43185
43257
|
return await this.sql.begin(async (tx) => {
|
|
43186
43258
|
const previousRows = await tx`
|
|
43187
|
-
SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at
|
|
43259
|
+
SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at, source
|
|
43188
43260
|
FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1
|
|
43189
43261
|
`;
|
|
43190
43262
|
const previous = previousRows[0];
|
|
43191
43263
|
const previousSha = typeof previous?.bundle_sha256 === "string" ? String(previous.bundle_sha256) : null;
|
|
43192
43264
|
const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? String(previous.revision_id) : null;
|
|
43193
43265
|
const tombstoned = previous?.tombstoned_at != null;
|
|
43266
|
+
if (input.seedBundledOnly && input.expectedRevisionId && !previous) {
|
|
43267
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, null);
|
|
43268
|
+
}
|
|
43269
|
+
if (input.seedBundledOnly && previous && (tombstoned || previous.source !== "bundled")) {
|
|
43270
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
43271
|
+
}
|
|
43194
43272
|
const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? String(previous.skill_md) : null;
|
|
43195
43273
|
if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
|
|
43196
43274
|
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
@@ -43231,6 +43309,12 @@ class PostgresSkillsStore {
|
|
|
43231
43309
|
body_blob = EXCLUDED.body_blob
|
|
43232
43310
|
`;
|
|
43233
43311
|
}
|
|
43312
|
+
if (input.seedBundledOnly) {
|
|
43313
|
+
const seedRows = await tx`SELECT revision_id FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} FOR UPDATE`;
|
|
43314
|
+
if (input.expectedRevisionId && !seedRows[0]) {
|
|
43315
|
+
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, null);
|
|
43316
|
+
}
|
|
43317
|
+
}
|
|
43234
43318
|
const rows = await tx`
|
|
43235
43319
|
INSERT INTO skills_registry (org_id, slug, display_name, description, category, tags_json, source, kind, version, skill_md,
|
|
43236
43320
|
bundle_sha256, bundle_byte_size, published_by_user_id, revision_id, revision_number, updated_at)
|
|
@@ -43259,8 +43343,11 @@ class PostgresSkillsStore {
|
|
|
43259
43343
|
tombstoned_at = NULL,
|
|
43260
43344
|
tombstone_purge_after = NULL,
|
|
43261
43345
|
updated_at = EXCLUDED.updated_at
|
|
43262
|
-
WHERE skills_registry.tombstoned_at IS NOT NULL
|
|
43263
|
-
OR skills_registry.revision_id = ${input.expectedRevisionId ?? NO_REVISION_SENTINEL2}
|
|
43346
|
+
WHERE (skills_registry.tombstoned_at IS NOT NULL
|
|
43347
|
+
OR skills_registry.revision_id = ${input.expectedRevisionId ?? NO_REVISION_SENTINEL2})
|
|
43348
|
+
AND (NOT ${Boolean(input.seedBundledOnly)} OR
|
|
43349
|
+
(skills_registry.tombstoned_at IS NULL AND skills_registry.source = 'bundled'
|
|
43350
|
+
AND skills_registry.revision_id = ${input.expectedRevisionId ?? NO_REVISION_SENTINEL2}))
|
|
43264
43351
|
RETURNING *
|
|
43265
43352
|
`;
|
|
43266
43353
|
if (!rows[0]) {
|
|
@@ -43675,6 +43762,15 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
43675
43762
|
}
|
|
43676
43763
|
return async function fetch2(request) {
|
|
43677
43764
|
const url = new URL(request.url);
|
|
43765
|
+
const versionedAliases = {
|
|
43766
|
+
"/v1/health": "/health",
|
|
43767
|
+
"/v1/auth/whoami": "/api/auth/whoami"
|
|
43768
|
+
};
|
|
43769
|
+
const alias = versionedAliases[url.pathname] ?? (url.pathname === "/v1" || url.pathname.startsWith("/v1/") ? `/api${url.pathname}` : undefined);
|
|
43770
|
+
if (alias) {
|
|
43771
|
+
url.pathname = alias;
|
|
43772
|
+
request = new Request(url, request);
|
|
43773
|
+
}
|
|
43678
43774
|
const segments = pathSegments(url.pathname);
|
|
43679
43775
|
try {
|
|
43680
43776
|
if (request.method === "GET" && url.pathname === "/health") {
|
|
@@ -52098,7 +52194,13 @@ class StopTaskCommand extends command3(_ep03, _mw03, "StopTask", StopTask$) {
|
|
|
52098
52194
|
|
|
52099
52195
|
// src/sdk/execution/dispatchers/ecs.ts
|
|
52100
52196
|
var CLIENT_TOKEN_BYTES = 16;
|
|
52101
|
-
var
|
|
52197
|
+
var LIVE_TASK_STATUSES = new Set(["PROVISIONING", "PENDING", "ACTIVATING", "RUNNING", "DEACTIVATING", "STOPPING", "DEPROVISIONING"]);
|
|
52198
|
+
function observedTask(states, taskArn) {
|
|
52199
|
+
if (states.length !== 1 || states[0]?.taskArn !== taskArn)
|
|
52200
|
+
return;
|
|
52201
|
+
const state = states[0];
|
|
52202
|
+
return state.lastStatus === "STOPPED" || LIVE_TASK_STATUSES.has(state.lastStatus) ? state : undefined;
|
|
52203
|
+
}
|
|
52102
52204
|
function clientTokenFor(runId2, attemptId) {
|
|
52103
52205
|
return createHash11("sha256").update(`${runId2}\x00${attemptId}`).digest("hex").slice(0, CLIENT_TOKEN_BYTES * 2);
|
|
52104
52206
|
}
|
|
@@ -52150,28 +52252,47 @@ class EcsDispatcher {
|
|
|
52150
52252
|
const run = await this.store.getRun(runId2);
|
|
52151
52253
|
if (!run)
|
|
52152
52254
|
return { accepted: false, detail: "no such run" };
|
|
52153
|
-
if (run.status === "cancelled")
|
|
52154
|
-
return { accepted: true, detail: "already cancelled" };
|
|
52155
52255
|
if (run.status === "succeeded" || run.status === "failed") {
|
|
52156
52256
|
return { accepted: false, detail: `run already ${run.status}` };
|
|
52157
52257
|
}
|
|
52158
52258
|
const attempts = await this.store.listAttempts(runId2);
|
|
52159
52259
|
const current = attempts[attempts.length - 1];
|
|
52160
52260
|
if (!current) {
|
|
52161
|
-
|
|
52162
|
-
|
|
52261
|
+
if (run.status === "cancelled")
|
|
52262
|
+
return { accepted: true, detail: "already cancelled (no attempt launched)" };
|
|
52263
|
+
const cancelled = await this.stateMachine.cancel(runId2);
|
|
52264
|
+
return cancelled.ok ? { accepted: true, detail: "cancelled (no attempt launched)" } : { accepted: false, detail: cancelled.reason };
|
|
52265
|
+
}
|
|
52266
|
+
if (!this.cancellationReceipt(await this.receipts.get(runId2, current.attemptId), run.admission, current)) {
|
|
52267
|
+
return { accepted: false, detail: "matching cancellation receipt is unavailable or contradictory" };
|
|
52163
52268
|
}
|
|
52164
|
-
const
|
|
52165
|
-
if (
|
|
52269
|
+
const observation = await this.reconcile(run.admission, current);
|
|
52270
|
+
if (observation.kind !== "already-launched" && observation.kind !== "previous-terminal") {
|
|
52271
|
+
return { accepted: false, detail: "task state unknown; cancellation is not yet confirmed" };
|
|
52272
|
+
}
|
|
52273
|
+
const taskArn = observation.kind === "already-launched" ? observation.taskId : (await this.store.listAttempts(runId2)).find((row) => row.attemptId === current.attemptId)?.taskId ?? null;
|
|
52274
|
+
if (!taskArn)
|
|
52275
|
+
return { accepted: false, detail: "stopped task identity is not confirmed" };
|
|
52276
|
+
if (observation.kind === "already-launched") {
|
|
52166
52277
|
try {
|
|
52167
|
-
await this.client.stopTask(
|
|
52278
|
+
await this.client.stopTask(observation.taskId);
|
|
52168
52279
|
} catch {}
|
|
52280
|
+
const stopped = await this.reconcile(run.admission, { ...current, taskId: observation.taskId });
|
|
52281
|
+
if (stopped.kind !== "previous-terminal") {
|
|
52282
|
+
return { accepted: false, target: observation.taskId, detail: "task stop is not yet confirmed; reconcile before retrying cancellation" };
|
|
52283
|
+
}
|
|
52169
52284
|
}
|
|
52170
|
-
|
|
52171
|
-
|
|
52172
|
-
|
|
52173
|
-
|
|
52174
|
-
|
|
52285
|
+
if (run.status !== "cancelled") {
|
|
52286
|
+
const cancelled = await this.stateMachine.cancel(runId2);
|
|
52287
|
+
if (!cancelled.ok)
|
|
52288
|
+
return { accepted: false, detail: cancelled.reason };
|
|
52289
|
+
}
|
|
52290
|
+
try {
|
|
52291
|
+
await this.writeCancellationReceipt(run.admission, current, taskArn);
|
|
52292
|
+
} catch {
|
|
52293
|
+
return { accepted: false, target: taskArn, detail: "task stopped; cancellation receipt is not yet confirmed" };
|
|
52294
|
+
}
|
|
52295
|
+
return { accepted: true, target: taskArn, detail: "cancelled after confirmed task stop and receipt" };
|
|
52175
52296
|
}
|
|
52176
52297
|
async launchAttempt(runId2) {
|
|
52177
52298
|
const run = await this.store.getRun(runId2);
|
|
@@ -52182,16 +52303,10 @@ class EcsDispatcher {
|
|
|
52182
52303
|
}
|
|
52183
52304
|
const attempts = await this.store.listAttempts(runId2);
|
|
52184
52305
|
const previous = attempts[attempts.length - 1];
|
|
52185
|
-
if (previous
|
|
52186
|
-
|
|
52187
|
-
if (reconciled.kind === "already-launched" || reconciled.kind === "previous-terminal") {
|
|
52188
|
-
return reconciled;
|
|
52189
|
-
}
|
|
52190
|
-
if (reconciled.kind === "ambiguous") {
|
|
52191
|
-
return reconciled;
|
|
52192
|
-
}
|
|
52306
|
+
if (previous) {
|
|
52307
|
+
return this.reconcile(run.admission, previous);
|
|
52193
52308
|
}
|
|
52194
|
-
const attemptNumber =
|
|
52309
|
+
const attemptNumber = 1;
|
|
52195
52310
|
const attempt = await this.store.createAttempt({ runId: runId2, attemptNumber });
|
|
52196
52311
|
const claimed = await this.stateMachine.claim({
|
|
52197
52312
|
runId: runId2,
|
|
@@ -52226,68 +52341,43 @@ class EcsDispatcher {
|
|
|
52226
52341
|
} catch {
|
|
52227
52342
|
await this.store.recordLaunchState({ runId: runId2, attemptId: attempt.attemptId, launchState: "ambiguous" });
|
|
52228
52343
|
const reconciled = await this.reconcile(run.admission, intent.attempt);
|
|
52229
|
-
|
|
52230
|
-
return reconciled;
|
|
52231
|
-
return reconciled.kind === "previous-terminal" ? reconciled : { kind: "launch-failed-absent", attemptId: attempt.attemptId };
|
|
52344
|
+
return reconciled;
|
|
52232
52345
|
}
|
|
52233
52346
|
await this.store.recordLaunchState({ runId: runId2, attemptId: attempt.attemptId, launchState: "launched", taskId: result.taskArn });
|
|
52234
52347
|
return { kind: "launched", attemptId: attempt.attemptId, taskId: result.taskArn };
|
|
52235
52348
|
}
|
|
52236
52349
|
async reconcile(admission, attempt) {
|
|
52237
|
-
|
|
52238
|
-
|
|
52350
|
+
const ambiguous = () => ({ kind: "ambiguous", attemptId: attempt.attemptId });
|
|
52351
|
+
let taskArn = attempt.taskId;
|
|
52352
|
+
if (!taskArn) {
|
|
52353
|
+
if (!attempt.startedBy)
|
|
52354
|
+
return ambiguous();
|
|
52355
|
+
let taskArns;
|
|
52239
52356
|
try {
|
|
52240
|
-
|
|
52357
|
+
taskArns = await this.client.listTasksByStartedBy(attempt.startedBy);
|
|
52241
52358
|
} catch {
|
|
52242
|
-
return
|
|
52359
|
+
return ambiguous();
|
|
52243
52360
|
}
|
|
52244
|
-
|
|
52245
|
-
|
|
52246
|
-
|
|
52247
|
-
}
|
|
52248
|
-
await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "terminal" });
|
|
52249
|
-
return { kind: "previous-terminal", attemptId: attempt.attemptId };
|
|
52250
|
-
}
|
|
52251
|
-
const token = attempt.startedBy;
|
|
52252
|
-
if (!token) {
|
|
52253
|
-
await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "absent" });
|
|
52254
|
-
return { kind: "launch-failed-absent", attemptId: attempt.attemptId };
|
|
52255
|
-
}
|
|
52256
|
-
let taskArns;
|
|
52257
|
-
try {
|
|
52258
|
-
taskArns = await this.client.listTasksByStartedBy(token);
|
|
52259
|
-
} catch {
|
|
52260
|
-
return { kind: "ambiguous", attemptId: attempt.attemptId };
|
|
52261
|
-
}
|
|
52262
|
-
if (taskArns.length === 0) {
|
|
52263
|
-
await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "absent" });
|
|
52264
|
-
return { kind: "launch-failed-absent", attemptId: attempt.attemptId };
|
|
52361
|
+
if (taskArns.length !== 1 || !taskArns[0])
|
|
52362
|
+
return ambiguous();
|
|
52363
|
+
taskArn = taskArns[0];
|
|
52265
52364
|
}
|
|
52266
52365
|
let states;
|
|
52267
52366
|
try {
|
|
52268
|
-
states = await this.client.describeTasks(
|
|
52367
|
+
states = await this.client.describeTasks([taskArn]);
|
|
52269
52368
|
} catch {
|
|
52270
|
-
return
|
|
52369
|
+
return ambiguous();
|
|
52271
52370
|
}
|
|
52272
|
-
const
|
|
52273
|
-
if (
|
|
52274
|
-
|
|
52275
|
-
|
|
52276
|
-
|
|
52371
|
+
const state = observedTask(states, taskArn);
|
|
52372
|
+
if (!state)
|
|
52373
|
+
return ambiguous();
|
|
52374
|
+
if (state.lastStatus !== "STOPPED") {
|
|
52375
|
+
await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "launched", taskId: taskArn });
|
|
52376
|
+
return { kind: "already-launched", attemptId: attempt.attemptId, taskId: taskArn };
|
|
52277
52377
|
}
|
|
52278
|
-
await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "terminal", taskId:
|
|
52378
|
+
await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "terminal", taskId: taskArn });
|
|
52279
52379
|
return { kind: "previous-terminal", attemptId: attempt.attemptId };
|
|
52280
52380
|
}
|
|
52281
|
-
async resolveTaskArn(admission, attempt) {
|
|
52282
|
-
if (attempt.taskId)
|
|
52283
|
-
return attempt.taskId;
|
|
52284
|
-
if (attempt.launchState === "launching" || attempt.launchState === "ambiguous") {
|
|
52285
|
-
const outcome = await this.reconcile(admission, attempt);
|
|
52286
|
-
if (outcome.kind === "already-launched")
|
|
52287
|
-
return outcome.taskId;
|
|
52288
|
-
}
|
|
52289
|
-
return null;
|
|
52290
|
-
}
|
|
52291
52381
|
runTaskInput(admission, attempt, clientToken, startedBy, requestDigest) {
|
|
52292
52382
|
const limits = admission.limits;
|
|
52293
52383
|
const cpuUnits = Math.max(256, Math.round(limits.maxCpuUnits / 256) * 256);
|
|
@@ -52312,81 +52402,103 @@ class EcsDispatcher {
|
|
|
52312
52402
|
]
|
|
52313
52403
|
};
|
|
52314
52404
|
}
|
|
52315
|
-
|
|
52316
|
-
|
|
52317
|
-
|
|
52318
|
-
|
|
52319
|
-
|
|
52320
|
-
attempt: { ...attempt, clientToken: attempt.clientToken ?? clientTokenFor(admission.runId, attempt.attemptId), requestDigest: attempt.requestDigest ?? "", startedBy: attempt.startedBy ?? "" },
|
|
52321
|
-
taskId: taskArn,
|
|
52322
|
-
launchedAt: this.now().toISOString()
|
|
52323
|
-
});
|
|
52405
|
+
cancellationReceipt(receipt, admission, attempt) {
|
|
52406
|
+
try {
|
|
52407
|
+
return !!receipt && receipt.runId === admission.runId && receipt.attemptId === attempt.attemptId && !!attempt.clientToken && receipt.clientToken === attempt.clientToken && !!attempt.requestDigest && receipt.requestDigest === attempt.requestDigest && !!attempt.startedBy && receipt.startedBy === attempt.startedBy && receipt.bundleDigest === admission.bundleDigest && receipt.runtimeImageDigest === admission.runtimeImageDigest && receipt.dependencyLayerTag === admission.dependencyLayerTag && canonicalJson(receipt.policy) === canonicalJson(admission.policy) && canonicalJson(receipt.limits) === canonicalJson(admission.limits) && (receipt.taskId === null || receipt.taskId === attempt.taskId) && (receipt.status === null && receipt.completedAt === null || receipt.status === "cancelled" && typeof receipt.completedAt === "string" && Number.isFinite(Date.parse(receipt.completedAt)));
|
|
52408
|
+
} catch {
|
|
52409
|
+
return false;
|
|
52324
52410
|
}
|
|
52325
|
-
await this.receipts.finalize({
|
|
52326
|
-
runId: admission.runId,
|
|
52327
|
-
attemptId: attempt.attemptId,
|
|
52328
|
-
status: "cancelled",
|
|
52329
|
-
exitCode: null,
|
|
52330
|
-
completedAt: this.now().toISOString()
|
|
52331
|
-
});
|
|
52332
52411
|
}
|
|
52333
|
-
|
|
52334
|
-
|
|
52335
|
-
|
|
52336
|
-
|
|
52337
|
-
|
|
52338
|
-
|
|
52412
|
+
async writeCancellationReceipt(admission, attempt, taskArn) {
|
|
52413
|
+
const existing = await this.receipts.get(admission.runId, attempt.attemptId);
|
|
52414
|
+
if (!this.cancellationReceipt(existing, admission, { ...attempt, taskId: taskArn }))
|
|
52415
|
+
throw Error("Cancellation receipt authority unavailable");
|
|
52416
|
+
const completedAt = existing.completedAt ?? this.now().toISOString();
|
|
52417
|
+
const expected = { ...existing, status: "cancelled", completedAt, exitCode: existing.status === "cancelled" ? existing.exitCode : null };
|
|
52418
|
+
const run = await this.store.getRun(admission.runId);
|
|
52419
|
+
if (existing.status !== "cancelled" || run?.status !== "cancelled" || run.terminalReceiptId !== attempt.attemptId) {
|
|
52420
|
+
await this.receipts.finalize({ runId: admission.runId, attemptId: attempt.attemptId, status: "cancelled", exitCode: expected.exitCode, completedAt });
|
|
52421
|
+
}
|
|
52422
|
+
const persisted = await this.receipts.get(admission.runId, attempt.attemptId);
|
|
52423
|
+
const finalized = await this.store.getRun(admission.runId);
|
|
52424
|
+
if (!persisted || canonicalJson(persisted) !== canonicalJson(expected) || finalized?.status !== "cancelled" || finalized.terminalReceiptId !== attempt.attemptId) {
|
|
52425
|
+
throw Error("Cancellation receipt persistence is unconfirmed");
|
|
52426
|
+
}
|
|
52427
|
+
}
|
|
52428
|
+
}
|
|
52429
|
+
function createAwsEcsClient(region, options = {}) {
|
|
52430
|
+
if (options.cluster !== undefined && !validText(options.cluster))
|
|
52431
|
+
throw Error("An explicit nonempty ECS cluster is required");
|
|
52432
|
+
const client = options.transport ?? new ECSClient({ region });
|
|
52433
|
+
let boundCluster = options.cluster;
|
|
52434
|
+
const cluster = (requested) => {
|
|
52435
|
+
const next = requested ?? boundCluster ?? "default";
|
|
52436
|
+
if (!validText(next) || boundCluster !== undefined && next !== boundCluster)
|
|
52437
|
+
throw Error("ECS client cluster binding changed");
|
|
52438
|
+
return boundCluster ??= next;
|
|
52439
|
+
};
|
|
52440
|
+
const response = (value) => {
|
|
52441
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
52442
|
+
throw Error("Incomplete ECS response");
|
|
52443
|
+
const result = value;
|
|
52444
|
+
if (result.failures !== undefined && (!Array.isArray(result.failures) || result.failures.length > 0))
|
|
52445
|
+
throw Error("ECS response contains failures");
|
|
52446
|
+
return result;
|
|
52447
|
+
};
|
|
52339
52448
|
return {
|
|
52340
52449
|
async runTask(input) {
|
|
52341
|
-
const
|
|
52342
|
-
cluster: input.cluster,
|
|
52450
|
+
const result = response(await client.send(new RunTaskCommand({
|
|
52451
|
+
cluster: cluster(input.cluster),
|
|
52343
52452
|
taskDefinition: input.taskDefinition,
|
|
52344
52453
|
launchType: input.launchType,
|
|
52345
52454
|
clientToken: input.clientToken,
|
|
52346
52455
|
startedBy: input.startedBy,
|
|
52347
|
-
networkConfiguration: {
|
|
52348
|
-
|
|
52349
|
-
|
|
52350
|
-
|
|
52351
|
-
|
|
52352
|
-
|
|
52353
|
-
},
|
|
52354
|
-
overrides: {
|
|
52355
|
-
containerOverrides: [
|
|
52356
|
-
{
|
|
52357
|
-
name: input.containerName,
|
|
52358
|
-
environment: input.environment,
|
|
52359
|
-
cpu: Number(input.cpu),
|
|
52360
|
-
memory: Number(input.memory)
|
|
52361
|
-
}
|
|
52362
|
-
]
|
|
52363
|
-
}
|
|
52364
|
-
}));
|
|
52365
|
-
const taskArn = response.tasks?.[0]?.taskArn ?? null;
|
|
52366
|
-
if (!taskArn) {
|
|
52367
|
-
const failure = response.failures?.[0];
|
|
52368
|
-
throw new Error(`RunTask refused: ${failure?.reason ?? "no task and no failure detail"}`);
|
|
52369
|
-
}
|
|
52370
|
-
return { taskArn };
|
|
52456
|
+
networkConfiguration: { awsvpcConfiguration: { subnets: input.subnets, securityGroups: input.securityGroups, assignPublicIp: "DISABLED" } },
|
|
52457
|
+
overrides: { containerOverrides: [{ name: input.containerName, environment: input.environment, cpu: Number(input.cpu), memory: Number(input.memory) }] }
|
|
52458
|
+
})));
|
|
52459
|
+
if (!Array.isArray(result.tasks) || result.tasks.length !== 1 || !validText(result.tasks[0]?.taskArn))
|
|
52460
|
+
throw Error("RunTask did not return one exact task");
|
|
52461
|
+
return { taskArn: result.tasks[0].taskArn };
|
|
52371
52462
|
},
|
|
52372
52463
|
async listTasksByStartedBy(startedBy) {
|
|
52373
|
-
const
|
|
52374
|
-
|
|
52464
|
+
const configuredCluster = cluster(), arns = [], tokens = new Set;
|
|
52465
|
+
let nextToken;
|
|
52466
|
+
for (let page = 0;page < 20; page++) {
|
|
52467
|
+
const result = response(await client.send(new ListTasksCommand({ cluster: configuredCluster, startedBy, ...nextToken ? { nextToken } : {} })));
|
|
52468
|
+
if (!Array.isArray(result.taskArns) || result.taskArns.length > 100 || !result.taskArns.every(validText))
|
|
52469
|
+
throw Error("Incomplete ECS task listing");
|
|
52470
|
+
arns.push(...result.taskArns);
|
|
52471
|
+
if (new Set(arns).size !== arns.length)
|
|
52472
|
+
throw Error("Repeated ECS task identity in listing");
|
|
52473
|
+
if (result.nextToken === undefined)
|
|
52474
|
+
return arns;
|
|
52475
|
+
if (!validText(result.nextToken) || tokens.has(result.nextToken))
|
|
52476
|
+
throw Error("Invalid ECS pagination cursor");
|
|
52477
|
+
tokens.add(result.nextToken);
|
|
52478
|
+
nextToken = result.nextToken;
|
|
52479
|
+
}
|
|
52480
|
+
throw Error("ECS task listing exceeded the bounded page limit");
|
|
52375
52481
|
},
|
|
52376
52482
|
async describeTasks(taskArns) {
|
|
52377
|
-
|
|
52378
|
-
|
|
52379
|
-
|
|
52380
|
-
|
|
52381
|
-
|
|
52382
|
-
|
|
52383
|
-
}));
|
|
52483
|
+
if (!taskArns.length || taskArns.length > 100 || !taskArns.every(validText) || new Set(taskArns).size !== taskArns.length)
|
|
52484
|
+
throw Error("Invalid ECS task identities");
|
|
52485
|
+
const result = response(await client.send(new DescribeTasksCommand({ cluster: cluster(), tasks: taskArns })));
|
|
52486
|
+
if (!Array.isArray(result.tasks) || result.tasks.length !== taskArns.length || new Set(result.tasks.map((task) => task?.taskArn)).size !== taskArns.length || result.tasks.some((task) => !taskArns.includes(task?.taskArn) || !validText(task?.lastStatus)))
|
|
52487
|
+
throw Error("Incomplete ECS task descriptions");
|
|
52488
|
+
return result.tasks.map((task) => ({ taskArn: task.taskArn, lastStatus: task.lastStatus, stopCode: task.stopCode ?? null, exitCode: task.containers?.[0]?.exitCode ?? null }));
|
|
52384
52489
|
},
|
|
52385
52490
|
async stopTask(taskArn) {
|
|
52386
|
-
|
|
52491
|
+
if (!validText(taskArn))
|
|
52492
|
+
throw Error("Invalid ECS task identity");
|
|
52493
|
+
const result = response(await client.send(new StopTaskCommand({ cluster: cluster(), task: taskArn })));
|
|
52494
|
+
if (result.task?.taskArn !== taskArn || !validText(result.task?.lastStatus))
|
|
52495
|
+
throw Error("Incomplete ECS stop response");
|
|
52387
52496
|
}
|
|
52388
52497
|
};
|
|
52389
52498
|
}
|
|
52499
|
+
function validText(value) {
|
|
52500
|
+
return typeof value === "string" && value.length > 0 && value.trim() === value && value.length <= 2048;
|
|
52501
|
+
}
|
|
52390
52502
|
// src/sdk/execution/dispatchers/e2b.ts
|
|
52391
52503
|
var E2B_LANE_STATUS = "e2b-lane-todo-infinity";
|
|
52392
52504
|
|
|
@@ -53078,8 +53190,17 @@ function creditCount(value) {
|
|
|
53078
53190
|
}
|
|
53079
53191
|
return value;
|
|
53080
53192
|
}
|
|
53193
|
+
function runQuoteReceipt(value) {
|
|
53194
|
+
if (value === undefined)
|
|
53195
|
+
return;
|
|
53196
|
+
if (typeof value !== "string" || !value.length || Buffer.byteLength(value, "utf8") > 4096) {
|
|
53197
|
+
throw new Error("Invalid quote receipt");
|
|
53198
|
+
}
|
|
53199
|
+
return value;
|
|
53200
|
+
}
|
|
53081
53201
|
function parseRemoteRunQuote(value) {
|
|
53082
53202
|
const quote = object(value);
|
|
53203
|
+
runQuoteReceipt(quote.quoteReceipt);
|
|
53083
53204
|
const pricing = object(quote.pricing);
|
|
53084
53205
|
if (typeof quote.skill !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(quote.skill))
|
|
53085
53206
|
throw new Error("Invalid quoted skill");
|
|
@@ -53226,6 +53347,69 @@ function parseUpdatedWorkspace(value) {
|
|
|
53226
53347
|
return { organization: { id: organization2.id, slug: organization2.slug, name: organization2.name } };
|
|
53227
53348
|
}
|
|
53228
53349
|
|
|
53350
|
+
// src/lib/remote-quote-errors.ts
|
|
53351
|
+
var quoteUnavailableMessages = Object.freeze({
|
|
53352
|
+
HOSTED_PROVIDER_UNAVAILABLE: "Hosted execution is temporarily unavailable on this Skills instance.",
|
|
53353
|
+
HOSTED_CONNECTORS_UNAVAILABLE: "Hosted connector execution is unavailable on this Skills instance.",
|
|
53354
|
+
SKILL_IMPLEMENTATION_UNAVAILABLE: "This skill has no hosted execution implementation.",
|
|
53355
|
+
HOSTED_PRICING_UNAVAILABLE: "Hosted execution is unavailable while this skill's pricing is reviewed.",
|
|
53356
|
+
RUNTIME_ALLOWLIST_REQUIRED: "Hosted execution is unavailable until this Skills instance enables its skill catalog.",
|
|
53357
|
+
RUNTIME_SKILL_NOT_ALLOWED: "This skill is not enabled for hosted execution on this Skills instance."
|
|
53358
|
+
});
|
|
53359
|
+
async function readQuoteUnavailableCode(response) {
|
|
53360
|
+
const maximum = 16 * 1024;
|
|
53361
|
+
const length = response.headers.get("content-length");
|
|
53362
|
+
if (response.status !== 503 || response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json" || length !== null && (!/^\d+$/.test(length) || Number(length) > maximum)) {
|
|
53363
|
+
response.body?.cancel().catch(() => {});
|
|
53364
|
+
return null;
|
|
53365
|
+
}
|
|
53366
|
+
const reader = response.body?.getReader();
|
|
53367
|
+
if (!reader)
|
|
53368
|
+
return null;
|
|
53369
|
+
let timer;
|
|
53370
|
+
let deadlineExceeded = false;
|
|
53371
|
+
const expired = Symbol("quote body deadline");
|
|
53372
|
+
const deadline = new Promise((resolve4) => {
|
|
53373
|
+
timer = setTimeout(() => {
|
|
53374
|
+
deadlineExceeded = true;
|
|
53375
|
+
resolve4(expired);
|
|
53376
|
+
reader.cancel().catch(() => {});
|
|
53377
|
+
}, 1500);
|
|
53378
|
+
});
|
|
53379
|
+
const chunks = [];
|
|
53380
|
+
let size = 0;
|
|
53381
|
+
try {
|
|
53382
|
+
while (true) {
|
|
53383
|
+
const next = await Promise.race([reader.read(), deadline]);
|
|
53384
|
+
if (next === expired || deadlineExceeded)
|
|
53385
|
+
return null;
|
|
53386
|
+
if (next.done)
|
|
53387
|
+
break;
|
|
53388
|
+
size += next.value.byteLength;
|
|
53389
|
+
if (size > maximum)
|
|
53390
|
+
return null;
|
|
53391
|
+
chunks.push(next.value);
|
|
53392
|
+
}
|
|
53393
|
+
const bytes = new Uint8Array(size);
|
|
53394
|
+
let offset = 0;
|
|
53395
|
+
for (const chunk of chunks) {
|
|
53396
|
+
bytes.set(chunk, offset);
|
|
53397
|
+
offset += chunk.byteLength;
|
|
53398
|
+
}
|
|
53399
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
53400
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
53401
|
+
return null;
|
|
53402
|
+
const code = value.code;
|
|
53403
|
+
return typeof code === "string" && Object.hasOwn(quoteUnavailableMessages, code) ? code : null;
|
|
53404
|
+
} catch {
|
|
53405
|
+
return null;
|
|
53406
|
+
} finally {
|
|
53407
|
+
clearTimeout(timer);
|
|
53408
|
+
reader.cancel().catch(() => {});
|
|
53409
|
+
reader.releaseLock();
|
|
53410
|
+
}
|
|
53411
|
+
}
|
|
53412
|
+
|
|
53229
53413
|
// src/lib/remote-client.ts
|
|
53230
53414
|
class RemoteRouteUnsupportedError extends Error {
|
|
53231
53415
|
path;
|
|
@@ -53251,6 +53435,18 @@ class RemoteRequestError extends Error {
|
|
|
53251
53435
|
}
|
|
53252
53436
|
}
|
|
53253
53437
|
|
|
53438
|
+
class RemoteQuoteUnavailableError extends RemoteRequestError {
|
|
53439
|
+
code;
|
|
53440
|
+
constructor(path, code) {
|
|
53441
|
+
super(path, 503);
|
|
53442
|
+
this.code = code;
|
|
53443
|
+
if (!Object.hasOwn(quoteUnavailableMessages, code))
|
|
53444
|
+
throw new Error("Unknown quote refusal code");
|
|
53445
|
+
this.name = "RemoteQuoteUnavailableError";
|
|
53446
|
+
this.message = quoteUnavailableMessages[code];
|
|
53447
|
+
}
|
|
53448
|
+
}
|
|
53449
|
+
|
|
53254
53450
|
class RemoteWorkspaceMemberError extends RemoteRequestError {
|
|
53255
53451
|
code;
|
|
53256
53452
|
constructor(path, code) {
|
|
@@ -53289,7 +53485,7 @@ class RemoteSkillsClient {
|
|
|
53289
53485
|
this.apiUrl = normalizeSkillsApiOrigin(apiUrl);
|
|
53290
53486
|
}
|
|
53291
53487
|
async request(path, options) {
|
|
53292
|
-
return fetch(
|
|
53488
|
+
return fetch(skillsApiRequestUrl(this.apiUrl, path), {
|
|
53293
53489
|
...options,
|
|
53294
53490
|
redirect: "error",
|
|
53295
53491
|
credentials: "omit",
|
|
@@ -53312,6 +53508,11 @@ class RemoteSkillsClient {
|
|
|
53312
53508
|
throw new RemoteRouteUnsupportedError(routePath, response.status, this.apiUrl);
|
|
53313
53509
|
}
|
|
53314
53510
|
if (!response.ok) {
|
|
53511
|
+
if (opts.quoteRefusal && options?.method === "POST" && /^\/api\/v1\/skills\/[^/?#]+\/quote$/.test(routePath) && response.status === 503) {
|
|
53512
|
+
const code = await readQuoteUnavailableCode(response);
|
|
53513
|
+
if (code)
|
|
53514
|
+
throw new RemoteQuoteUnavailableError(routePath, code);
|
|
53515
|
+
}
|
|
53315
53516
|
if (path === "/api/v1/billing/checkout" && options?.method === "POST" && response.status === 503 && await responseBodyCarriesCode(response, ["SUBSCRIPTION_CHECKOUT_UNAVAILABLE"])) {
|
|
53316
53517
|
throw new RemoteCapabilityUnavailableError;
|
|
53317
53518
|
}
|
|
@@ -53344,6 +53545,7 @@ class RemoteSkillsClient {
|
|
|
53344
53545
|
return { status: res.status, body };
|
|
53345
53546
|
}
|
|
53346
53547
|
async submitRun(slug, input, args, approval = {}) {
|
|
53548
|
+
const quoteReceipt = runQuoteReceipt(approval.quoteReceipt);
|
|
53347
53549
|
if (approval.idempotencyKey !== undefined && !/^[A-Za-z0-9._:-]{1,128}$/.test(approval.idempotencyKey))
|
|
53348
53550
|
throw new Error("Idempotency key must be 1-128 URL-safe characters");
|
|
53349
53551
|
if (approval.maxCostCents !== undefined)
|
|
@@ -53360,16 +53562,21 @@ class RemoteSkillsClient {
|
|
|
53360
53562
|
...approval.maxCredits !== undefined ? { maxCredits: approval.maxCredits } : {},
|
|
53361
53563
|
...approval.maxCostCents !== undefined ? { maxCostCents: approval.maxCostCents } : {},
|
|
53362
53564
|
...approval.idempotencyKey !== undefined ? { idempotencyKey: approval.idempotencyKey } : {},
|
|
53363
|
-
...approval.inputFiles !== undefined ? { files: approval.inputFiles } : {}
|
|
53565
|
+
...approval.inputFiles !== undefined ? { files: approval.inputFiles } : {},
|
|
53566
|
+
...quoteReceipt !== undefined ? { quoteReceipt } : {}
|
|
53364
53567
|
})
|
|
53365
53568
|
});
|
|
53569
|
+
if (!res.ok) {
|
|
53570
|
+
res.body?.cancel().catch(() => {});
|
|
53571
|
+
throw new RemoteRequestError(`/api/v1/runs/${encodeURIComponent(slug)}`, res.status);
|
|
53572
|
+
}
|
|
53366
53573
|
return normalizeRemoteSkillRunContract(await res.json(), slug);
|
|
53367
53574
|
}
|
|
53368
|
-
async quoteRun(slug, input = {}, args = []) {
|
|
53575
|
+
async quoteRun(slug, input = {}, args = [], files) {
|
|
53369
53576
|
const response = await this.requestNewRoute(`/api/v1/skills/${encodeURIComponent(slug)}/quote`, {
|
|
53370
53577
|
method: "POST",
|
|
53371
|
-
body: JSON.stringify({ input, args })
|
|
53372
|
-
});
|
|
53578
|
+
body: JSON.stringify({ input, args, ...files === undefined ? {} : { files } })
|
|
53579
|
+
}, { quoteRefusal: true });
|
|
53373
53580
|
return parseRemoteRunQuote(await response.json());
|
|
53374
53581
|
}
|
|
53375
53582
|
getCapabilities() {
|
|
@@ -53384,17 +53591,24 @@ class RemoteSkillsClient {
|
|
|
53384
53591
|
return this.capabilities;
|
|
53385
53592
|
}
|
|
53386
53593
|
async submitQuotedRun(slug, input = {}, args = [], approval = {}) {
|
|
53594
|
+
runQuoteReceipt(approval.quoteReceipt);
|
|
53595
|
+
({ input, args, approval } = JSON.parse(JSON.stringify({ input, args, approval })));
|
|
53387
53596
|
const maximum = creditCount(approval.maxCredits ?? approval.maxCostCents ?? 0);
|
|
53388
53597
|
if (approval.maxCostCents !== undefined && approval.maxCostCents !== maximum)
|
|
53389
53598
|
throw new Error("Credit approval fields disagree");
|
|
53390
|
-
const quote = await this.quoteRun(slug, input, args);
|
|
53391
|
-
if (quote.pricing.costCents > maximum)
|
|
53599
|
+
const quote = approval.quoteReceipt === undefined ? await this.quoteRun(slug, input, args, approval.inputFiles?.length ? approval.inputFiles : undefined) : undefined;
|
|
53600
|
+
if (quote && quote.pricing.costCents > maximum)
|
|
53392
53601
|
throw new RemoteCreditApprovalError(quote.pricing.costCents, maximum);
|
|
53393
53602
|
const capabilities = await this.getCapabilities();
|
|
53394
53603
|
if (!capabilities.capabilities.includes("runs.submit") || capabilities.billing?.boundedRunApproval !== true || capabilities.billing.unit !== "credits") {
|
|
53395
53604
|
throw new Error("The configured server does not support bounded credit approval; refusing remote submission");
|
|
53396
53605
|
}
|
|
53397
|
-
return this.submitRun(quote
|
|
53606
|
+
return this.submitRun(quote?.skill ?? slug, input, args, {
|
|
53607
|
+
...approval,
|
|
53608
|
+
maxCredits: maximum,
|
|
53609
|
+
maxCostCents: maximum,
|
|
53610
|
+
...(quote?.quoteReceipt ?? approval.quoteReceipt) === undefined ? {} : { quoteReceipt: quote?.quoteReceipt ?? approval.quoteReceipt }
|
|
53611
|
+
});
|
|
53398
53612
|
}
|
|
53399
53613
|
async getIdentity() {
|
|
53400
53614
|
return (await this.requestNewRoute("/api/auth/whoami")).json();
|
|
@@ -53682,6 +53896,10 @@ class RemoteSkillsClient {
|
|
|
53682
53896
|
return { id: artifactId2, fileName: String(artifact.fileName ?? artifactId2), bytes, byteSize: bytes.byteLength, sha256: artifact.sha256 };
|
|
53683
53897
|
}
|
|
53684
53898
|
async submitQuotedRunWithFiles(slug, input, args, files, approval = {}) {
|
|
53899
|
+
runQuoteReceipt(approval.quoteReceipt);
|
|
53900
|
+
({ input, args, approval } = JSON.parse(JSON.stringify({ input, args, approval })));
|
|
53901
|
+
describeRemoteFiles(files);
|
|
53902
|
+
files = files.map((file) => ({ name: file.name, contentType: file.contentType, bytes: new Uint8Array(file.bytes) }));
|
|
53685
53903
|
const inputFiles = describeRemoteFiles(files);
|
|
53686
53904
|
if (files.length && !(await this.getCapabilities()).capabilities.includes("runs.uploads"))
|
|
53687
53905
|
throw new Error("The configured server does not support input uploads");
|
|
@@ -53745,7 +53963,7 @@ class RemoteSkillsClient {
|
|
|
53745
53963
|
const headers = { Authorization: `Bearer ${this.apiKey}` };
|
|
53746
53964
|
if (ifMatch)
|
|
53747
53965
|
headers["If-Match"] = ifMatch;
|
|
53748
|
-
return fetch(
|
|
53966
|
+
return fetch(skillsApiRequestUrl(this.apiUrl, "/api/v1/skills"), {
|
|
53749
53967
|
method: "POST",
|
|
53750
53968
|
headers,
|
|
53751
53969
|
body: form,
|
|
@@ -54030,7 +54248,7 @@ async function requestInvitationEmail(origin, action, input) {
|
|
|
54030
54248
|
}
|
|
54031
54249
|
const body = JSON.stringify({ invitationId: value.invitationId, token: value.token, challengeId: value.challengeId, ...action === "accept" ? { code: value.code } : {} });
|
|
54032
54250
|
try {
|
|
54033
|
-
const response = await fetch(
|
|
54251
|
+
const response = await fetch(skillsApiRequestUrl(target, `/api/v1/account/invitations/email-${action}`), {
|
|
54034
54252
|
method: "POST",
|
|
54035
54253
|
headers: { "Content-Type": "application/json" },
|
|
54036
54254
|
body,
|
|
@@ -54061,6 +54279,312 @@ async function requestInvitationEmail(origin, action, input) {
|
|
|
54061
54279
|
throw new RemoteInvitationEmailUnconfirmedError(action);
|
|
54062
54280
|
}
|
|
54063
54281
|
|
|
54282
|
+
// src/lib/remote-private-publications.ts
|
|
54283
|
+
import { createHash as createHash14 } from "crypto";
|
|
54284
|
+
var PRIVATE_PUBLICATION_MAX_BYTES = 16 * 1024 * 1024;
|
|
54285
|
+
var failures = {
|
|
54286
|
+
INVALID_REQUEST: [400, "The publication request is invalid."],
|
|
54287
|
+
SESSION_EXPIRED: [401, "Sign in again to manage this publication."],
|
|
54288
|
+
ACCOUNT_UNAVAILABLE: [403, "The account is unavailable."],
|
|
54289
|
+
INTERACTIVE_SESSION_REQUIRED: [403, "An interactive account session is required."],
|
|
54290
|
+
PUBLICATION_FORBIDDEN: [403, "This session cannot manage the publication."],
|
|
54291
|
+
PUBLICATION_ENTITLEMENT_REQUIRED: [403, "The workspace is not entitled to publish private skills."],
|
|
54292
|
+
PUBLICATION_UNAVAILABLE: [404, "The publication is unavailable to this session."],
|
|
54293
|
+
MANIFEST_NAME_MISMATCH: [409, "The manifest name does not match the selected skill."],
|
|
54294
|
+
IDEMPOTENCY_CONFLICT: [409, "This request key already identifies different publication bytes. Use the saved recovery directory."],
|
|
54295
|
+
CURRENT_VERSION_CHANGED: [409, "The current version changed. Inspect it before explicitly starting another publication."],
|
|
54296
|
+
VERSION_EXISTS: [409, "This version already exists."],
|
|
54297
|
+
VERSION_RESERVED: [409, "This version is reserved by another publication."],
|
|
54298
|
+
PUBLICATION_COMMITTED: [409, "The publication is already committed."],
|
|
54299
|
+
PUBLICATION_UPLOAD_UNAVAILABLE: [409, "This publication cannot receive another upload."],
|
|
54300
|
+
PUBLICATION_LIMIT: [429, "The workspace publication limit has been reached."],
|
|
54301
|
+
PUBLICATION_BUSY: [503, "The publication is busy. Reconcile the saved intent before retrying."],
|
|
54302
|
+
PUBLICATION_UNCERTAIN: [503, "The publication outcome is uncertain. Reconcile the saved intent."],
|
|
54303
|
+
PUBLICATION_CAPABILITY_UNAVAILABLE: [503, "Private publishing is not enabled on this server."],
|
|
54304
|
+
PUBLICATION_SIGNING_UNAVAILABLE: [503, "Upload authorization is temporarily unavailable. Keep the same intent."]
|
|
54305
|
+
};
|
|
54306
|
+
|
|
54307
|
+
class PrivatePublicationError extends Error {
|
|
54308
|
+
code;
|
|
54309
|
+
uncertain;
|
|
54310
|
+
status;
|
|
54311
|
+
constructor(code, message, uncertain = false, status) {
|
|
54312
|
+
super(message);
|
|
54313
|
+
this.code = code;
|
|
54314
|
+
this.uncertain = uncertain;
|
|
54315
|
+
this.status = status;
|
|
54316
|
+
this.name = "PrivatePublicationError";
|
|
54317
|
+
}
|
|
54318
|
+
}
|
|
54319
|
+
var bad = () => {
|
|
54320
|
+
throw new PrivatePublicationError("INVALID_PUBLICATION_INPUT", "Invalid publication input or recovery data.");
|
|
54321
|
+
};
|
|
54322
|
+
var invalid3 = () => {
|
|
54323
|
+
throw new PrivatePublicationError("INVALID_PUBLICATION_RESPONSE", "The server returned an invalid publication result.");
|
|
54324
|
+
};
|
|
54325
|
+
var publicationUuid = (v2) => typeof v2 === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(v2);
|
|
54326
|
+
var hash = (v2) => typeof v2 === "string" && /^[a-f0-9]{64}$/.test(v2);
|
|
54327
|
+
var record5 = (v2) => !!v2 && typeof v2 === "object" && !Array.isArray(v2);
|
|
54328
|
+
var exact = (v2, keys) => record5(v2) && Object.keys(v2).sort().join(",") === keys.sort().join(",");
|
|
54329
|
+
var date = (v2) => typeof v2 === "string" && /^\d{4}-\d\d-\d\dT[0-9:.]+(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.test(v2) && Number.isFinite(Date.parse(v2));
|
|
54330
|
+
var publicationSha256 = (bytes) => createHash14("sha256").update(bytes).digest("hex");
|
|
54331
|
+
function checkedPublicationDeclaration(value) {
|
|
54332
|
+
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)
|
|
54333
|
+
return bad();
|
|
54334
|
+
try {
|
|
54335
|
+
const manifest = JSON.parse(value.manifestText);
|
|
54336
|
+
if (!record5(manifest) || manifest.version !== value.version || validatePortableManifestContract(manifest, { strict: true }).length || !hash(manifest.provenance?.content_hash))
|
|
54337
|
+
return bad();
|
|
54338
|
+
} catch {
|
|
54339
|
+
return bad();
|
|
54340
|
+
}
|
|
54341
|
+
return Object.freeze({ ...value });
|
|
54342
|
+
}
|
|
54343
|
+
function checkedPublicationView(value, skillId, intentId, declaration) {
|
|
54344
|
+
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)
|
|
54345
|
+
return invalid3();
|
|
54346
|
+
if (declaration && ["version", "expectedCurrentVersionId", "archiveSha256", "archiveByteSize"].some((k4) => value[k4] !== declaration[k4]))
|
|
54347
|
+
return invalid3();
|
|
54348
|
+
return Object.freeze({ ...value });
|
|
54349
|
+
}
|
|
54350
|
+
async function boundedJson(url, init, token, mutation, budget = 15000, signal) {
|
|
54351
|
+
const controller = new AbortController;
|
|
54352
|
+
let reader;
|
|
54353
|
+
let response;
|
|
54354
|
+
const timeout = new PrivatePublicationError("PUBLICATION_UNCONFIRMED", "No confirmed publication result. Inspect the saved intent before retrying.", mutation);
|
|
54355
|
+
let timer;
|
|
54356
|
+
let abort;
|
|
54357
|
+
try {
|
|
54358
|
+
return await Promise.race([(async () => {
|
|
54359
|
+
if (signal?.aborted)
|
|
54360
|
+
throw timeout;
|
|
54361
|
+
response = await fetch(url, {
|
|
54362
|
+
...init,
|
|
54363
|
+
redirect: "error",
|
|
54364
|
+
credentials: "omit",
|
|
54365
|
+
signal: controller.signal,
|
|
54366
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }
|
|
54367
|
+
});
|
|
54368
|
+
const length = response.headers.get("content-length");
|
|
54369
|
+
if (length !== null && (!/^\d+$/.test(length) || Number(length) > 65536))
|
|
54370
|
+
return invalid3();
|
|
54371
|
+
reader = response.body?.getReader();
|
|
54372
|
+
const chunks = [];
|
|
54373
|
+
let size = 0;
|
|
54374
|
+
if (reader)
|
|
54375
|
+
while (true) {
|
|
54376
|
+
const part = await reader.read();
|
|
54377
|
+
if (part.done)
|
|
54378
|
+
break;
|
|
54379
|
+
size += part.value.byteLength;
|
|
54380
|
+
if (size > 65536)
|
|
54381
|
+
return invalid3();
|
|
54382
|
+
chunks.push(part.value);
|
|
54383
|
+
}
|
|
54384
|
+
if (controller.signal.aborted)
|
|
54385
|
+
throw timeout;
|
|
54386
|
+
const bytes = Buffer.concat(chunks);
|
|
54387
|
+
let body;
|
|
54388
|
+
try {
|
|
54389
|
+
body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
54390
|
+
} catch {
|
|
54391
|
+
return invalid3();
|
|
54392
|
+
}
|
|
54393
|
+
if (!response.ok) {
|
|
54394
|
+
const code = record5(body) && typeof body.code === "string" && Object.hasOwn(failures, body.code) ? body.code : null;
|
|
54395
|
+
if (code && failures[code][0] === response.status)
|
|
54396
|
+
throw new PrivatePublicationError(code, failures[code][1], mutation && code === "PUBLICATION_UNCERTAIN", response.status);
|
|
54397
|
+
throw new PrivatePublicationError("PUBLICATION_REQUEST_FAILED", "The publication request was refused. Inspect its status before retrying.", mutation && response.status >= 500, response.status);
|
|
54398
|
+
}
|
|
54399
|
+
return body;
|
|
54400
|
+
})(), new Promise((_, reject) => {
|
|
54401
|
+
abort = () => {
|
|
54402
|
+
controller.abort();
|
|
54403
|
+
reject(timeout);
|
|
54404
|
+
};
|
|
54405
|
+
timer = setTimeout(abort, Math.max(1, Math.min(15000, budget)));
|
|
54406
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
54407
|
+
})]);
|
|
54408
|
+
} catch (error) {
|
|
54409
|
+
if (error instanceof PrivatePublicationError) {
|
|
54410
|
+
if (mutation && error.code === "INVALID_PUBLICATION_RESPONSE")
|
|
54411
|
+
throw timeout;
|
|
54412
|
+
throw error;
|
|
54413
|
+
}
|
|
54414
|
+
throw timeout;
|
|
54415
|
+
} finally {
|
|
54416
|
+
if (timer)
|
|
54417
|
+
clearTimeout(timer);
|
|
54418
|
+
if (abort)
|
|
54419
|
+
signal?.removeEventListener("abort", abort);
|
|
54420
|
+
controller.abort();
|
|
54421
|
+
if (reader)
|
|
54422
|
+
reader.cancel().catch(() => {});
|
|
54423
|
+
else
|
|
54424
|
+
response?.body?.cancel().catch(() => {});
|
|
54425
|
+
}
|
|
54426
|
+
}
|
|
54427
|
+
|
|
54428
|
+
class RemotePrivatePublicationsClient {
|
|
54429
|
+
apiOrigin;
|
|
54430
|
+
organizationId;
|
|
54431
|
+
userId;
|
|
54432
|
+
membershipId;
|
|
54433
|
+
#token;
|
|
54434
|
+
constructor(apiUrl, session) {
|
|
54435
|
+
const checked = parseWorkspaceSession(session, { userId: session?.user?.id, membershipId: session?.user?.membershipId });
|
|
54436
|
+
if (checked.user.role === "viewer")
|
|
54437
|
+
throw new PrivatePublicationError("PUBLICATION_FORBIDDEN", failures.PUBLICATION_FORBIDDEN[1]);
|
|
54438
|
+
this.apiOrigin = normalizeSkillsApiOrigin(apiUrl);
|
|
54439
|
+
this.#token = checked.token;
|
|
54440
|
+
this.organizationId = checked.organization.id;
|
|
54441
|
+
this.userId = checked.user.id;
|
|
54442
|
+
this.membershipId = checked.user.membershipId;
|
|
54443
|
+
Object.freeze(this);
|
|
54444
|
+
}
|
|
54445
|
+
async getCapability(options = {}) {
|
|
54446
|
+
if (options.timeoutMs !== undefined && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1 || options.timeoutMs > 15000))
|
|
54447
|
+
return bad();
|
|
54448
|
+
const response = await boundedJson(skillsApiRequestUrl(this.apiOrigin, "/api/v1/capabilities"), {}, this.#token, false, options.timeoutMs, options.signal);
|
|
54449
|
+
const p2 = record5(response) && response.privatePublishing;
|
|
54450
|
+
if (!record5(response) || response.contractVersion !== 1 || response.apiVersion !== 1 || !exact(p2, ["contractVersion", "enabled", "authentication", "maxArchiveBytes", "uploadMaxTtlSeconds", "executionEnabled"]) || p2.contractVersion !== 1 || typeof p2.enabled !== "boolean" || p2.authentication !== "interactive-session" || p2.maxArchiveBytes !== PRIVATE_PUBLICATION_MAX_BYTES || p2.uploadMaxTtlSeconds !== 300 || p2.executionEnabled !== false)
|
|
54451
|
+
throw new PrivatePublicationError("PUBLICATION_CONTRACT_UNAVAILABLE", "This server does not support the hosted private publication contract.");
|
|
54452
|
+
return Object.freeze({ ...p2 });
|
|
54453
|
+
}
|
|
54454
|
+
async#gate(enabled, options = {}) {
|
|
54455
|
+
if (!(await this.getCapability(options)).enabled && enabled)
|
|
54456
|
+
throw new PrivatePublicationError("PUBLICATION_CAPABILITY_UNAVAILABLE", failures.PUBLICATION_CAPABILITY_UNAVAILABLE[1]);
|
|
54457
|
+
}
|
|
54458
|
+
#path(skillId, intentId) {
|
|
54459
|
+
if (!publicationUuid(skillId) || intentId !== undefined && !publicationUuid(intentId))
|
|
54460
|
+
return bad();
|
|
54461
|
+
return skillsApiRequestUrl(this.apiOrigin, `/api/v1/skills/${skillId}/publication-uploads${intentId ? `/${intentId}` : ""}`);
|
|
54462
|
+
}
|
|
54463
|
+
async#view(path, method, skillId, intentId, declaration, options = {}) {
|
|
54464
|
+
const value = await boundedJson(path, { method, ...method === "GET" ? {} : { body: JSON.stringify(declaration ?? {}) } }, this.#token, method !== "GET", options.timeoutMs, options.signal);
|
|
54465
|
+
try {
|
|
54466
|
+
if (!record5(value) || !Object.keys(value).every((k4) => k4 === "upload" || k4 === "changed") || value.changed !== undefined && typeof value.changed !== "boolean")
|
|
54467
|
+
return invalid3();
|
|
54468
|
+
return checkedPublicationView(value.upload, skillId, intentId, declaration);
|
|
54469
|
+
} catch (error) {
|
|
54470
|
+
if (method !== "GET")
|
|
54471
|
+
throw new PrivatePublicationError("PUBLICATION_UNCONFIRMED", "The publication result could not be confirmed. Reconcile the saved intent before another action.", true);
|
|
54472
|
+
throw error;
|
|
54473
|
+
}
|
|
54474
|
+
}
|
|
54475
|
+
async begin(skillId, input) {
|
|
54476
|
+
const path = this.#path(skillId), declaration = checkedPublicationDeclaration(input);
|
|
54477
|
+
await this.#gate(true);
|
|
54478
|
+
return this.#view(path, "POST", skillId, undefined, declaration);
|
|
54479
|
+
}
|
|
54480
|
+
async get(skillId, intentId, options = {}) {
|
|
54481
|
+
const path = this.#path(skillId, intentId), until = Date.now() + (options.timeoutMs ?? 15000);
|
|
54482
|
+
await this.#gate(false, options);
|
|
54483
|
+
return this.#view(path, "GET", skillId, intentId, undefined, { ...options, timeoutMs: Math.max(1, until - Date.now()) });
|
|
54484
|
+
}
|
|
54485
|
+
async finalize(skillId, intentId) {
|
|
54486
|
+
const path = this.#path(skillId, intentId);
|
|
54487
|
+
await this.#gate(true);
|
|
54488
|
+
return this.#view(`${path}/finalize`, "POST", skillId, intentId);
|
|
54489
|
+
}
|
|
54490
|
+
async cancel(skillId, intentId) {
|
|
54491
|
+
const path = this.#path(skillId, intentId);
|
|
54492
|
+
await this.#gate(false);
|
|
54493
|
+
return this.#view(path, "DELETE", skillId, intentId);
|
|
54494
|
+
}
|
|
54495
|
+
async upload(skillId, intent, bytes) {
|
|
54496
|
+
const captured = checkedPublicationView(intent, skillId), path = this.#path(skillId, captured.id);
|
|
54497
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength !== captured.archiveByteSize)
|
|
54498
|
+
return bad();
|
|
54499
|
+
const owned = Buffer.from(bytes);
|
|
54500
|
+
if (captured.state !== "awaiting_upload" || owned.byteLength !== captured.archiveByteSize || publicationSha256(owned) !== captured.archiveSha256)
|
|
54501
|
+
return bad();
|
|
54502
|
+
await this.#gate(true);
|
|
54503
|
+
const value = await boundedJson(`${path}/upload-url`, { method: "POST", body: "{}" }, this.#token, false);
|
|
54504
|
+
const upload = this.#upload(value, captured);
|
|
54505
|
+
const controller = new AbortController;
|
|
54506
|
+
let timer;
|
|
54507
|
+
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);
|
|
54508
|
+
try {
|
|
54509
|
+
await Promise.race([(async () => {
|
|
54510
|
+
const response = await fetch(upload.uploadUrl, { method: "PUT", headers: upload.headers, body: owned, redirect: "error", credentials: "omit", signal: controller.signal });
|
|
54511
|
+
response.body?.cancel().catch(() => {});
|
|
54512
|
+
if (response.status !== 200 || controller.signal.aborted)
|
|
54513
|
+
throw uncertain();
|
|
54514
|
+
})(), new Promise((_, reject) => {
|
|
54515
|
+
timer = setTimeout(() => {
|
|
54516
|
+
controller.abort();
|
|
54517
|
+
reject(uncertain());
|
|
54518
|
+
}, 30000);
|
|
54519
|
+
})]);
|
|
54520
|
+
} catch {
|
|
54521
|
+
throw uncertain();
|
|
54522
|
+
} finally {
|
|
54523
|
+
if (timer)
|
|
54524
|
+
clearTimeout(timer);
|
|
54525
|
+
controller.abort();
|
|
54526
|
+
}
|
|
54527
|
+
}
|
|
54528
|
+
#upload(value, intent) {
|
|
54529
|
+
if (!exact(value, ["upload"]) || !exact(value.upload, ["method", "uploadUrl", "headers", "expiresAt"]))
|
|
54530
|
+
return invalid3();
|
|
54531
|
+
const p2 = value.upload;
|
|
54532
|
+
if (p2.method !== "PUT" || typeof p2.uploadUrl !== "string" || p2.uploadUrl.length > 8192 || /[\x00-\x20\x7f]/.test(p2.uploadUrl) || !date(p2.expiresAt) || Date.parse(p2.expiresAt) - Date.now() < 1000 || Date.parse(p2.expiresAt) - Date.now() > 300000 || Date.parse(p2.expiresAt) > Date.parse(intent.expiresAt) || !exact(p2.headers, ["content-type", "content-length", "x-amz-checksum-sha256", "x-amz-expected-bucket-owner"]) || p2.headers["content-type"] !== "application/gzip" || p2.headers["content-length"] !== String(intent.archiveByteSize) || p2.headers["x-amz-checksum-sha256"] !== Buffer.from(intent.archiveSha256, "hex").toString("base64") || typeof p2.headers["x-amz-expected-bucket-owner"] !== "string" || !/^\d{12}$/.test(p2.headers["x-amz-expected-bucket-owner"]))
|
|
54533
|
+
return invalid3();
|
|
54534
|
+
let url;
|
|
54535
|
+
try {
|
|
54536
|
+
url = new URL(p2.uploadUrl);
|
|
54537
|
+
} catch {
|
|
54538
|
+
return invalid3();
|
|
54539
|
+
}
|
|
54540
|
+
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") ?? ""))
|
|
54541
|
+
return invalid3();
|
|
54542
|
+
const queryKeys = ["X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Date", "X-Amz-Expires", "X-Amz-Security-Token", "X-Amz-Signature", "X-Amz-SignedHeaders"];
|
|
54543
|
+
if ([...url.searchParams.keys()].sort().join(",") !== queryKeys.sort().join(","))
|
|
54544
|
+
return invalid3();
|
|
54545
|
+
const issued = url.searchParams.get("X-Amz-Date"), ttl = url.searchParams.get("X-Amz-Expires"), credential = url.searchParams.get("X-Amz-Credential");
|
|
54546
|
+
const timestamp3 = /^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)Z$/.exec(issued);
|
|
54547
|
+
const region = url.hostname.split(".s3.")[1].split(".amazonaws.com")[0];
|
|
54548
|
+
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")))
|
|
54549
|
+
return invalid3();
|
|
54550
|
+
const issuedAt = Date.parse(`${timestamp3[1]}-${timestamp3[2]}-${timestamp3[3]}T${timestamp3[4]}:${timestamp3[5]}:${timestamp3[6]}Z`);
|
|
54551
|
+
if (!Number.isFinite(issuedAt) || issuedAt > Date.now() + 1000 || issuedAt + Number(ttl) * 1000 !== Date.parse(p2.expiresAt))
|
|
54552
|
+
return invalid3();
|
|
54553
|
+
return { method: "PUT", uploadUrl: url.href, expiresAt: p2.expiresAt, headers: Object.freeze({ ...p2.headers }) };
|
|
54554
|
+
}
|
|
54555
|
+
async wait(skillId, intentId, options = {}) {
|
|
54556
|
+
const timeout = options.timeoutMs ?? 60000;
|
|
54557
|
+
if (!Number.isSafeInteger(timeout) || timeout < 0 || timeout > 300000)
|
|
54558
|
+
return bad();
|
|
54559
|
+
const until = Date.now() + timeout;
|
|
54560
|
+
let previous;
|
|
54561
|
+
while (true) {
|
|
54562
|
+
if (options.signal?.aborted)
|
|
54563
|
+
throw new PrivatePublicationError("PUBLICATION_WAIT_ABORTED", "Stopped waiting. The server publication continues; inspect the saved intent.");
|
|
54564
|
+
let view;
|
|
54565
|
+
try {
|
|
54566
|
+
view = await this.get(skillId, intentId, { timeoutMs: timeout === 0 ? 15000 : Math.max(1, Math.min(15000, until - Date.now())), signal: options.signal });
|
|
54567
|
+
} catch (error) {
|
|
54568
|
+
if (previous && Date.now() >= until && !options.signal?.aborted)
|
|
54569
|
+
return previous;
|
|
54570
|
+
throw error;
|
|
54571
|
+
}
|
|
54572
|
+
previous = view;
|
|
54573
|
+
if (!["queued", "verifying"].includes(view.state) || Date.now() >= until)
|
|
54574
|
+
return view;
|
|
54575
|
+
await new Promise((resolve4) => {
|
|
54576
|
+
const timer = setTimeout(done, Math.min(1000, until - Date.now()));
|
|
54577
|
+
function done() {
|
|
54578
|
+
clearTimeout(timer);
|
|
54579
|
+
options.signal?.removeEventListener("abort", done);
|
|
54580
|
+
resolve4();
|
|
54581
|
+
}
|
|
54582
|
+
options.signal?.addEventListener("abort", done, { once: true });
|
|
54583
|
+
});
|
|
54584
|
+
}
|
|
54585
|
+
}
|
|
54586
|
+
}
|
|
54587
|
+
|
|
54064
54588
|
// src/lib/remote-auth.ts
|
|
54065
54589
|
var MAX_ERROR_DETAIL_LENGTH = 200;
|
|
54066
54590
|
|
|
@@ -54083,10 +54607,11 @@ class HostedApiError extends Error {
|
|
|
54083
54607
|
async function requestAuthApi(instance, path, options) {
|
|
54084
54608
|
const url = normalizeSkillsApiOrigin(instance);
|
|
54085
54609
|
const safeUrl = url;
|
|
54086
|
-
const
|
|
54610
|
+
const requestUrl = skillsApiRequestUrl(url, path);
|
|
54611
|
+
const endpoint = `${(options?.method || "GET").toUpperCase()} ${requestUrl}`;
|
|
54087
54612
|
let res;
|
|
54088
54613
|
try {
|
|
54089
|
-
res = await fetch(
|
|
54614
|
+
res = await fetch(requestUrl, {
|
|
54090
54615
|
...options,
|
|
54091
54616
|
redirect: "error",
|
|
54092
54617
|
signal: options?.signal ?? AbortSignal.timeout(15000),
|
|
@@ -54101,10 +54626,10 @@ async function requestAuthApi(instance, path, options) {
|
|
|
54101
54626
|
const text2 = await res.text();
|
|
54102
54627
|
const body = text2 ? parseJsonBody(text2) : {};
|
|
54103
54628
|
if (!res.ok) {
|
|
54104
|
-
const
|
|
54105
|
-
const detail = typeof
|
|
54106
|
-
const error = typeof
|
|
54107
|
-
const code = typeof
|
|
54629
|
+
const record6 = isRecord6(body) ? body : {};
|
|
54630
|
+
const detail = typeof record6.detail === "string" ? record6.detail : undefined;
|
|
54631
|
+
const error = typeof record6.error === "string" ? record6.error : undefined;
|
|
54632
|
+
const code = typeof record6.code === "string" ? record6.code : undefined;
|
|
54108
54633
|
throw new HostedApiError(detail || error || `${res.status} ${res.statusText}`, {
|
|
54109
54634
|
status: res.status,
|
|
54110
54635
|
code,
|
|
@@ -54138,6 +54663,10 @@ class RemoteSkillsAuthClient {
|
|
|
54138
54663
|
constructor(apiUrl) {
|
|
54139
54664
|
this.apiOrigin = normalizeSkillsApiOrigin(apiUrl);
|
|
54140
54665
|
}
|
|
54666
|
+
async openPrivatePublications(email2, code, context) {
|
|
54667
|
+
const origin = this.apiOrigin, captured = workspaceContext(context);
|
|
54668
|
+
return new RemotePrivatePublicationsClient(origin, await this.switchWorkspace(email2, code, captured));
|
|
54669
|
+
}
|
|
54141
54670
|
requestInvitationEmailChallenge(input) {
|
|
54142
54671
|
return requestInvitationEmail(this.apiOrigin, "challenge", input);
|
|
54143
54672
|
}
|
|
@@ -54185,9 +54714,10 @@ class RemoteSkillsAuthClient {
|
|
|
54185
54714
|
const apiOrigin = this.apiOrigin;
|
|
54186
54715
|
if (typeof email2 !== "string" || !email2.includes("@") || typeof code !== "string" || !/^\d{6}$/.test(code))
|
|
54187
54716
|
throw new Error("Fresh email and six-digit verification code are required to manage this account");
|
|
54717
|
+
const requestUrl = skillsApiRequestUrl(apiOrigin, "/api/auth/verify");
|
|
54188
54718
|
let response;
|
|
54189
54719
|
try {
|
|
54190
|
-
response = await fetch(
|
|
54720
|
+
response = await fetch(requestUrl, {
|
|
54191
54721
|
method: "POST",
|
|
54192
54722
|
redirect: "error",
|
|
54193
54723
|
credentials: "omit",
|
|
@@ -54278,6 +54808,242 @@ class RemoteSkillsAuthClient {
|
|
|
54278
54808
|
return requestAuthApi(this.apiOrigin, path, options);
|
|
54279
54809
|
}
|
|
54280
54810
|
}
|
|
54811
|
+
// src/lib/private-publication-recovery.ts
|
|
54812
|
+
import { constants as constants2, closeSync as closeSync3, fsyncSync, fstatSync as fstatSync3, lstatSync as lstatSync5, mkdirSync as mkdirSync8, openSync as openSync3, readSync as readSync2, realpathSync as realpathSync2, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
|
|
54813
|
+
import { dirname as dirname10, isAbsolute as isAbsolute5, join as join21, resolve as resolve4 } from "path";
|
|
54814
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
54815
|
+
var fail = () => {
|
|
54816
|
+
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.");
|
|
54817
|
+
};
|
|
54818
|
+
function safeDirectory(directory) {
|
|
54819
|
+
if (!isAbsolute5(directory) || directory !== resolve4(directory) || realpathSync2(directory) !== directory)
|
|
54820
|
+
return fail();
|
|
54821
|
+
for (let path = directory;; path = dirname10(path)) {
|
|
54822
|
+
const stat = lstatSync5(path);
|
|
54823
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
54824
|
+
return fail();
|
|
54825
|
+
if (path === dirname10(path))
|
|
54826
|
+
break;
|
|
54827
|
+
}
|
|
54828
|
+
const own = lstatSync5(directory);
|
|
54829
|
+
if ((own.mode & 63) !== 0 || process.getuid && own.uid !== process.getuid())
|
|
54830
|
+
return fail();
|
|
54831
|
+
return { dev: own.dev, ino: own.ino };
|
|
54832
|
+
}
|
|
54833
|
+
function unchangedDirectory(directory, identity) {
|
|
54834
|
+
const now2 = safeDirectory(directory);
|
|
54835
|
+
if (now2.dev !== identity.dev || now2.ino !== identity.ino)
|
|
54836
|
+
return fail();
|
|
54837
|
+
}
|
|
54838
|
+
function readOwned(directory, name, max) {
|
|
54839
|
+
const identity = safeDirectory(directory), file = join21(directory, name);
|
|
54840
|
+
const fd = openSync3(file, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
54841
|
+
try {
|
|
54842
|
+
const stat = fstatSync3(fd);
|
|
54843
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > max || stat.size < 1 || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
|
|
54844
|
+
return fail();
|
|
54845
|
+
const buffer = Buffer.alloc(Math.min(max, stat.size) + 1);
|
|
54846
|
+
let length = 0;
|
|
54847
|
+
while (length < buffer.length) {
|
|
54848
|
+
const count = readSync2(fd, buffer, length, buffer.length - length, length);
|
|
54849
|
+
if (count === 0)
|
|
54850
|
+
break;
|
|
54851
|
+
length += count;
|
|
54852
|
+
}
|
|
54853
|
+
const bytes = buffer.subarray(0, length), after = fstatSync3(fd);
|
|
54854
|
+
unchangedDirectory(directory, identity);
|
|
54855
|
+
if (bytes.length !== stat.size || stat.size !== after.size || stat.mtimeMs !== after.mtimeMs || stat.ctimeMs !== after.ctimeMs)
|
|
54856
|
+
return fail();
|
|
54857
|
+
return bytes;
|
|
54858
|
+
} finally {
|
|
54859
|
+
closeSync3(fd);
|
|
54860
|
+
}
|
|
54861
|
+
}
|
|
54862
|
+
function writeOwned(directory, name, bytes) {
|
|
54863
|
+
const fd = openSync3(join21(directory, name), constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
54864
|
+
try {
|
|
54865
|
+
writeFileSync6(fd, bytes);
|
|
54866
|
+
fsyncSync(fd);
|
|
54867
|
+
} finally {
|
|
54868
|
+
closeSync3(fd);
|
|
54869
|
+
}
|
|
54870
|
+
}
|
|
54871
|
+
function save(directory, value) {
|
|
54872
|
+
const identity = safeDirectory(directory), temporary = `.receipt-${randomUUID6()}.json`;
|
|
54873
|
+
writeOwned(directory, temporary, JSON.stringify(value) + `
|
|
54874
|
+
`);
|
|
54875
|
+
unchangedDirectory(directory, identity);
|
|
54876
|
+
renameSync3(join21(directory, temporary), join21(directory, "receipt.json"));
|
|
54877
|
+
const fd = openSync3(directory, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
54878
|
+
try {
|
|
54879
|
+
fsyncSync(fd);
|
|
54880
|
+
} finally {
|
|
54881
|
+
closeSync3(fd);
|
|
54882
|
+
}
|
|
54883
|
+
}
|
|
54884
|
+
function bind(client, receipt) {
|
|
54885
|
+
if (["apiOrigin", "organizationId", "userId", "membershipId"].some((k4) => client[k4] !== receipt[k4]))
|
|
54886
|
+
throw new PrivatePublicationError("PUBLICATION_IDENTITY_CHANGED", "The fresh session does not match the recovery directory's server, account and membership.");
|
|
54887
|
+
}
|
|
54888
|
+
async function locked(directory, action) {
|
|
54889
|
+
const identity = safeDirectory(directory), file = join21(directory, "operation.lock");
|
|
54890
|
+
let fd;
|
|
54891
|
+
try {
|
|
54892
|
+
fd = openSync3(file, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
54893
|
+
} catch {
|
|
54894
|
+
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.");
|
|
54895
|
+
}
|
|
54896
|
+
const lock = fstatSync3(fd);
|
|
54897
|
+
try {
|
|
54898
|
+
writeFileSync6(fd, JSON.stringify({ pid: process.pid }) + `
|
|
54899
|
+
`);
|
|
54900
|
+
fsyncSync(fd);
|
|
54901
|
+
return await action();
|
|
54902
|
+
} finally {
|
|
54903
|
+
closeSync3(fd);
|
|
54904
|
+
unchangedDirectory(directory, identity);
|
|
54905
|
+
const now2 = lstatSync5(file);
|
|
54906
|
+
if (now2.dev !== lock.dev || now2.ino !== lock.ino || !now2.isFile() || now2.isSymbolicLink())
|
|
54907
|
+
fail();
|
|
54908
|
+
unlinkSync(file);
|
|
54909
|
+
}
|
|
54910
|
+
}
|
|
54911
|
+
function readPrivatePublicationRecovery(directory) {
|
|
54912
|
+
try {
|
|
54913
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readOwned(directory, "receipt.json", 65536)));
|
|
54914
|
+
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))
|
|
54915
|
+
return fail();
|
|
54916
|
+
value.declaration = checkedPublicationDeclaration(value.declaration);
|
|
54917
|
+
if (value.intent !== null)
|
|
54918
|
+
value.intent = checkedPublicationView(value.intent, value.skillId, undefined, value.declaration);
|
|
54919
|
+
if (value.intent === null && !["prepared", "begin_uncertain"].includes(value.phase))
|
|
54920
|
+
return fail();
|
|
54921
|
+
const bytes = readOwned(directory, "bundle.tgz", PRIVATE_PUBLICATION_MAX_BYTES);
|
|
54922
|
+
if (bytes.length !== value.declaration.archiveByteSize || publicationSha256(bytes) !== value.declaration.archiveSha256)
|
|
54923
|
+
return fail();
|
|
54924
|
+
return { receipt: value, bytes };
|
|
54925
|
+
} catch {
|
|
54926
|
+
return fail();
|
|
54927
|
+
}
|
|
54928
|
+
}
|
|
54929
|
+
async function preparePrivatePublication(client, sourceDirectory, recoveryDirectory, input) {
|
|
54930
|
+
if (!publicationUuid(input.skillId) || !(input.expectedCurrentVersionId === null || publicationUuid(input.expectedCurrentVersionId)) || input.idempotencyKey !== undefined && !publicationUuid(input.idempotencyKey))
|
|
54931
|
+
return fail();
|
|
54932
|
+
const packed = packSkillBundle(sourceDirectory, { maxUnpackedBytes: 32 * 1024 * 1024 });
|
|
54933
|
+
const inspected = await inspectSkillBundle(packed.bytes, { limits: { compressedBytes: PRIVATE_PUBLICATION_MAX_BYTES } });
|
|
54934
|
+
const manifest = inspected.entries.find((entry) => entry.path === "skill.json");
|
|
54935
|
+
if (!manifest || manifest.bytes.length > 16384 || !(await verifyContentHashFromEntries(inspected.entries)).valid)
|
|
54936
|
+
throw new PrivatePublicationError("PUBLICATION_BUNDLE_INVALID", "The skill must have a valid skill.json with its current content hash. Validate the skill before publishing.");
|
|
54937
|
+
const manifestText = new TextDecoder("utf-8", { fatal: true }).decode(manifest.bytes);
|
|
54938
|
+
const declaration = checkedPublicationDeclaration({
|
|
54939
|
+
idempotencyKey: input.idempotencyKey ?? randomUUID6(),
|
|
54940
|
+
version: JSON.parse(manifestText).version,
|
|
54941
|
+
expectedCurrentVersionId: input.expectedCurrentVersionId,
|
|
54942
|
+
manifestText,
|
|
54943
|
+
archiveSha256: inspected.sha256,
|
|
54944
|
+
archiveByteSize: packed.bytes.length
|
|
54945
|
+
});
|
|
54946
|
+
const receipt = {
|
|
54947
|
+
contractVersion: 1,
|
|
54948
|
+
apiOrigin: client.apiOrigin,
|
|
54949
|
+
organizationId: client.organizationId,
|
|
54950
|
+
userId: client.userId,
|
|
54951
|
+
membershipId: client.membershipId,
|
|
54952
|
+
skillId: input.skillId,
|
|
54953
|
+
declaration,
|
|
54954
|
+
phase: "prepared",
|
|
54955
|
+
intent: null
|
|
54956
|
+
};
|
|
54957
|
+
if (!isAbsolute5(recoveryDirectory) || resolve4(recoveryDirectory) !== recoveryDirectory || realpathSync2(dirname10(recoveryDirectory)) !== dirname10(recoveryDirectory))
|
|
54958
|
+
return fail();
|
|
54959
|
+
mkdirSync8(recoveryDirectory, { mode: 448 });
|
|
54960
|
+
safeDirectory(recoveryDirectory);
|
|
54961
|
+
const parent = openSync3(dirname10(recoveryDirectory), constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
54962
|
+
try {
|
|
54963
|
+
fsyncSync(parent);
|
|
54964
|
+
} finally {
|
|
54965
|
+
closeSync3(parent);
|
|
54966
|
+
}
|
|
54967
|
+
writeOwned(recoveryDirectory, "bundle.tgz", packed.bytes);
|
|
54968
|
+
save(recoveryDirectory, receipt);
|
|
54969
|
+
return receipt;
|
|
54970
|
+
}
|
|
54971
|
+
function privatePublicationResult(directory, receipt) {
|
|
54972
|
+
const state = receipt.intent?.state ?? receipt.phase, committed = state === "committed";
|
|
54973
|
+
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.";
|
|
54974
|
+
return {
|
|
54975
|
+
recoveryDirectory: directory,
|
|
54976
|
+
skillId: receipt.skillId,
|
|
54977
|
+
intentId: receipt.intent?.id ?? null,
|
|
54978
|
+
state,
|
|
54979
|
+
versionId: receipt.intent?.versionId ?? null,
|
|
54980
|
+
committed,
|
|
54981
|
+
executionEnabled: false,
|
|
54982
|
+
nextAction
|
|
54983
|
+
};
|
|
54984
|
+
}
|
|
54985
|
+
async function continuePrivatePublication(client, directory, options) {
|
|
54986
|
+
if (options.confirm !== true)
|
|
54987
|
+
throw new PrivatePublicationError("PUBLICATION_CONFIRM_REQUIRED", "Explicit upload confirmation is required.");
|
|
54988
|
+
if (options.waitMs !== undefined && (!Number.isSafeInteger(options.waitMs) || options.waitMs < 0 || options.waitMs > 300000))
|
|
54989
|
+
return fail();
|
|
54990
|
+
return locked(directory, () => continueLocked(client, directory, options));
|
|
54991
|
+
}
|
|
54992
|
+
async function continueLocked(client, directory, options) {
|
|
54993
|
+
const { receipt, bytes } = readPrivatePublicationRecovery(directory);
|
|
54994
|
+
bind(client, receipt);
|
|
54995
|
+
if (!receipt.intent) {
|
|
54996
|
+
receipt.phase = "begin_uncertain";
|
|
54997
|
+
save(directory, receipt);
|
|
54998
|
+
receipt.intent = await client.begin(receipt.skillId, receipt.declaration);
|
|
54999
|
+
receipt.phase = "awaiting_upload";
|
|
55000
|
+
save(directory, receipt);
|
|
55001
|
+
} else {
|
|
55002
|
+
receipt.intent = checkedPublicationView(await client.get(receipt.skillId, receipt.intent.id), receipt.skillId, receipt.intent.id, receipt.declaration);
|
|
55003
|
+
save(directory, receipt);
|
|
55004
|
+
}
|
|
55005
|
+
if (receipt.intent.state === "awaiting_upload") {
|
|
55006
|
+
if (receipt.phase === "awaiting_upload") {
|
|
55007
|
+
receipt.phase = "upload_uncertain";
|
|
55008
|
+
save(directory, receipt);
|
|
55009
|
+
try {
|
|
55010
|
+
await client.upload(receipt.skillId, receipt.intent, bytes);
|
|
55011
|
+
} catch (error) {
|
|
55012
|
+
if (error instanceof PrivatePublicationError && !error.uncertain) {
|
|
55013
|
+
receipt.phase = "awaiting_upload";
|
|
55014
|
+
save(directory, receipt);
|
|
55015
|
+
}
|
|
55016
|
+
throw error;
|
|
55017
|
+
}
|
|
55018
|
+
receipt.phase = "uploaded";
|
|
55019
|
+
save(directory, receipt);
|
|
55020
|
+
}
|
|
55021
|
+
receipt.phase = "finalize_uncertain";
|
|
55022
|
+
save(directory, receipt);
|
|
55023
|
+
receipt.intent = checkedPublicationView(await client.finalize(receipt.skillId, receipt.intent.id), receipt.skillId, receipt.intent.id, receipt.declaration);
|
|
55024
|
+
receipt.phase = "observed";
|
|
55025
|
+
save(directory, receipt);
|
|
55026
|
+
}
|
|
55027
|
+
if (options.waitMs !== undefined && options.waitMs > 0 && ["queued", "verifying"].includes(receipt.intent.state)) {
|
|
55028
|
+
receipt.intent = checkedPublicationView(await client.wait(receipt.skillId, receipt.intent.id, { timeoutMs: options.waitMs }), receipt.skillId, receipt.intent.id, receipt.declaration);
|
|
55029
|
+
receipt.phase = "observed";
|
|
55030
|
+
save(directory, receipt);
|
|
55031
|
+
}
|
|
55032
|
+
return privatePublicationResult(directory, receipt);
|
|
55033
|
+
}
|
|
55034
|
+
async function inspectPrivatePublication(client, directory, cancel = false) {
|
|
55035
|
+
return locked(directory, () => inspectLocked(client, directory, cancel));
|
|
55036
|
+
}
|
|
55037
|
+
async function inspectLocked(client, directory, cancel) {
|
|
55038
|
+
const { receipt } = readPrivatePublicationRecovery(directory);
|
|
55039
|
+
bind(client, receipt);
|
|
55040
|
+
if (receipt.intent) {
|
|
55041
|
+
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);
|
|
55042
|
+
save(directory, receipt);
|
|
55043
|
+
} else if (cancel)
|
|
55044
|
+
throw new PrivatePublicationError("PUBLICATION_INTENT_UNKNOWN", "Reconcile the saved begin request with publication resume before cancelling its intent.");
|
|
55045
|
+
return privatePublicationResult(directory, receipt);
|
|
55046
|
+
}
|
|
54281
55047
|
export {
|
|
54282
55048
|
verifyContentHashFromEntries,
|
|
54283
55049
|
validateRunLifecycleEvent,
|
|
@@ -54307,7 +55073,9 @@ export {
|
|
|
54307
55073
|
registerRoutes,
|
|
54308
55074
|
redactRunOutput,
|
|
54309
55075
|
receiptId,
|
|
55076
|
+
readPrivatePublicationRecovery,
|
|
54310
55077
|
protocolStateOf,
|
|
55078
|
+
preparePrivatePublication,
|
|
54311
55079
|
packSkillBundle,
|
|
54312
55080
|
noticeLocalSkillsMode,
|
|
54313
55081
|
normalizeSkillsApiOrigin,
|
|
@@ -54321,6 +55089,7 @@ export {
|
|
|
54321
55089
|
isSkillsLocalOptIn,
|
|
54322
55090
|
isActiveStatus,
|
|
54323
55091
|
inspectSkillBundle,
|
|
55092
|
+
inspectPrivatePublication,
|
|
54324
55093
|
getServerSkillMd,
|
|
54325
55094
|
getServerSkill,
|
|
54326
55095
|
expiresAtFor,
|
|
@@ -54345,6 +55114,7 @@ export {
|
|
|
54345
55114
|
createGovernanceStore,
|
|
54346
55115
|
createCancelService,
|
|
54347
55116
|
createAwsEcsClient,
|
|
55117
|
+
continuePrivatePublication,
|
|
54348
55118
|
configuredSkillsApiUrl,
|
|
54349
55119
|
computeContentHashFromEntries,
|
|
54350
55120
|
clientTokenFor,
|
|
@@ -54381,6 +55151,8 @@ export {
|
|
|
54381
55151
|
RemoteSkillsAuthClient,
|
|
54382
55152
|
RemoteRouteUnsupportedError,
|
|
54383
55153
|
RemoteRequestError,
|
|
55154
|
+
RemoteQuoteUnavailableError,
|
|
55155
|
+
RemotePrivatePublicationsClient,
|
|
54384
55156
|
RemoteInvitationEmailUnconfirmedError,
|
|
54385
55157
|
RemoteInvitationEmailError,
|
|
54386
55158
|
RemoteCreditApprovalError,
|
|
@@ -54390,8 +55162,10 @@ export {
|
|
|
54390
55162
|
RUN_LIFECYCLE_EVENT_TYPES,
|
|
54391
55163
|
RUN_LIFECYCLE_EVENT_FIELDS,
|
|
54392
55164
|
REMOTE_SKILL_RUN_CONTRACT_VERSION,
|
|
55165
|
+
PrivatePublicationError,
|
|
54393
55166
|
PostgresSkillsStore,
|
|
54394
55167
|
PostgresGovernanceStore,
|
|
55168
|
+
PRIVATE_PUBLICATION_MAX_BYTES,
|
|
54395
55169
|
MissingSkillsFleetError,
|
|
54396
55170
|
MemorySkillsStore,
|
|
54397
55171
|
MemoryRunExecutionStore,
|