@hasna/skills 0.5.2 → 0.5.3

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/dist/index.js CHANGED
@@ -1463,8 +1463,45 @@ function validateRegistryConsistency(registry, skillsDir) {
1463
1463
  import { createHash } from "crypto";
1464
1464
  import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
1465
1465
  import { join as join5, sep } from "path";
1466
+
1467
+ // src/lib/skill-entry-path.ts
1468
+ class SkillEntryPaths {
1469
+ files = new Set;
1470
+ directories = new Set;
1471
+ add(path, maxBytes, invalid, limit) {
1472
+ if (path.length > maxBytes)
1473
+ limit();
1474
+ const encoded = new TextEncoder().encode(path);
1475
+ if (encoded.byteLength > maxBytes)
1476
+ limit();
1477
+ if (new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(encoded) !== path)
1478
+ invalid("Invalid UTF-8 entry path");
1479
+ if (!path || /[\\:\x00-\x1f\x7f]/u.test(path))
1480
+ invalid("Unsafe entry path");
1481
+ if (path.split("/").some((segment) => !segment || segment === "." || segment === ".."))
1482
+ invalid("Unsafe entry path segment");
1483
+ const key = path.normalize("NFC").toLowerCase().normalize("NFC");
1484
+ if (this.files.has(key) || this.directories.has(key))
1485
+ invalid("Duplicate or conflicting entry path");
1486
+ const parents = key.split("/");
1487
+ parents.pop();
1488
+ while (parents.length) {
1489
+ const parent = parents.join("/");
1490
+ if (this.files.has(parent))
1491
+ invalid("Conflicting entry file ancestor");
1492
+ this.directories.add(parent);
1493
+ parents.pop();
1494
+ }
1495
+ this.files.add(key);
1496
+ }
1497
+ }
1498
+
1499
+ // src/lib/skill-hash.ts
1466
1500
  var CONTENT_HASH_ALGORITHM = "sha256";
1467
1501
  var HASH_EXCLUDE_DIRS = new Set([".git", "node_modules", "dist", "build", ".turbo"]);
1502
+ function excludedHashEntry(name, directory) {
1503
+ return name.startsWith(".") || directory && HASH_EXCLUDE_DIRS.has(name);
1504
+ }
1468
1505
  var HASH_COVERAGE = [
1469
1506
  "SKILL.md",
1470
1507
  "skill.json",
@@ -1546,7 +1583,7 @@ function collectDirectory(files, dir, rel) {
1546
1583
  if (stats.isSymbolicLink())
1547
1584
  continue;
1548
1585
  if (stats.isDirectory()) {
1549
- if (HASH_EXCLUDE_DIRS.has(entry))
1586
+ if (excludedHashEntry(entry, true))
1550
1587
  continue;
1551
1588
  collectDirectory(files, absolute, childRel);
1552
1589
  } else if (stats.isFile()) {
@@ -1556,28 +1593,242 @@ function collectDirectory(files, dir, rel) {
1556
1593
  }
1557
1594
  function collectFile(files, absolute, rel) {
1558
1595
  const buffer = readFileSync4(absolute);
1596
+ files.push(normalizeBundleFile(rel.split(sep).join("/"), buffer));
1597
+ }
1598
+ function normalizeBundleFile(rel, buffer) {
1559
1599
  if (rel === "skill.json") {
1560
- files.push({ rel: rel.split(sep).join("/"), content: new TextEncoder().encode(canonicalizeManifest(new TextDecoder().decode(buffer))) });
1561
- return;
1600
+ return { rel, content: new TextEncoder().encode(canonicalizeManifest(new TextDecoder().decode(buffer))) };
1562
1601
  }
1563
1602
  if (looksLikeText(buffer)) {
1564
1603
  const normalized = normalizeLineEndings(new TextDecoder().decode(buffer));
1565
- files.push({ rel: rel.split(sep).join("/"), content: new TextEncoder().encode(normalized) });
1566
- return;
1604
+ return { rel, content: new TextEncoder().encode(normalized) };
1567
1605
  }
1568
- files.push({ rel: rel.split(sep).join("/"), content: buffer });
1606
+ return { rel, content: buffer };
1569
1607
  }
1570
1608
  function computeContentHash(skillPath) {
1609
+ return hashBundleFiles(collectBundleFiles(skillPath));
1610
+ }
1611
+ function* bundleHashParts(files) {
1612
+ for (const file of files) {
1613
+ yield new TextEncoder().encode(file.rel);
1614
+ yield new TextEncoder().encode(`\x00${file.content.length}\x00`);
1615
+ yield file.content;
1616
+ yield new TextEncoder().encode("\x00");
1617
+ }
1618
+ yield new TextEncoder().encode("\x00");
1619
+ }
1620
+ function hashBundleFiles(files) {
1571
1621
  const hash = createHash(CONTENT_HASH_ALGORITHM);
1572
- for (const file of collectBundleFiles(skillPath)) {
1573
- hash.update(new TextEncoder().encode(file.rel));
1574
- hash.update(new TextEncoder().encode(`\x00${file.content.length}\x00`));
1575
- hash.update(file.content);
1576
- hash.update(new TextEncoder().encode("\x00"));
1622
+ for (const part of bundleHashParts(files))
1623
+ hash.update(part);
1624
+ return hash.digest("hex");
1625
+ }
1626
+ async function hashBundleFilesCooperatively(files, check) {
1627
+ const hash = createHash(CONTENT_HASH_ALGORITHM);
1628
+ let bytesSinceYield = 0;
1629
+ for (const part of bundleHashParts(files)) {
1630
+ for (let offset = 0;offset < part.byteLength; offset += 64 * 1024) {
1631
+ check();
1632
+ const chunk = part.subarray(offset, offset + 64 * 1024);
1633
+ hash.update(chunk);
1634
+ bytesSinceYield += chunk.byteLength;
1635
+ if (bytesSinceYield >= 256 * 1024) {
1636
+ await new Promise((resolve2) => setImmediate(resolve2));
1637
+ bytesSinceYield = 0;
1638
+ }
1639
+ }
1577
1640
  }
1578
- hash.update(new TextEncoder().encode("\x00"));
1641
+ check();
1579
1642
  return hash.digest("hex");
1580
1643
  }
1644
+ var CONTENT_HASH_LIMITS = Object.freeze({
1645
+ entries: 1024,
1646
+ rawBytes: 64 * 1024 * 1024,
1647
+ normalizedBytes: 64 * 1024 * 1024,
1648
+ fileBytes: 16 * 1024 * 1024,
1649
+ normalizedFileBytes: 16 * 1024 * 1024,
1650
+ pathBytes: 100,
1651
+ manifestBytes: 16 * 1024,
1652
+ manifestDepth: 64,
1653
+ timeoutMs: 5000
1654
+ });
1655
+
1656
+ class ContentHashInputError extends Error {
1657
+ code;
1658
+ constructor(code, message) {
1659
+ super(message);
1660
+ this.code = code;
1661
+ this.name = "ContentHashInputError";
1662
+ }
1663
+ }
1664
+ function invalidContent(message = "Invalid content hash input") {
1665
+ throw new ContentHashInputError("CONTENT_HASH_INVALID", message);
1666
+ }
1667
+ function contentLimit(message) {
1668
+ throw new ContentHashInputError("CONTENT_HASH_LIMIT", message);
1669
+ }
1670
+ function contentRecord(value, allowed) {
1671
+ if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
1672
+ invalidContent();
1673
+ const result = Object.create(null);
1674
+ for (const key of Reflect.ownKeys(value)) {
1675
+ if (typeof key !== "string" || !allowed.includes(key))
1676
+ invalidContent();
1677
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1678
+ if (!descriptor || !("value" in descriptor))
1679
+ invalidContent("Accessor content hash input is unsupported");
1680
+ result[key] = descriptor.value;
1681
+ }
1682
+ return result;
1683
+ }
1684
+ function contentOptions(options) {
1685
+ const record = contentRecord(options, ["limits", "signal"]);
1686
+ const limits = { ...CONTENT_HASH_LIMITS };
1687
+ if (record.limits !== undefined) {
1688
+ const supplied = contentRecord(record.limits, Object.keys(limits));
1689
+ for (const key of Object.keys(supplied)) {
1690
+ const value = supplied[key];
1691
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0 || value > limits[key])
1692
+ contentLimit("Invalid content hash limit");
1693
+ limits[key] = value;
1694
+ }
1695
+ }
1696
+ if (record.signal !== undefined && !(record.signal instanceof AbortSignal))
1697
+ invalidContent("Invalid content hash signal");
1698
+ return { limits, signal: record.signal };
1699
+ }
1700
+ var typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
1701
+ var byteLengthOf = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteLength").get;
1702
+ var bufferOf = Object.getOwnPropertyDescriptor(typedArrayPrototype, "buffer").get;
1703
+ function snapshotContentEntries(entries, limits, check) {
1704
+ if (!Array.isArray(entries))
1705
+ invalidContent("Content hash entries must be an array");
1706
+ if (entries.length > limits.entries)
1707
+ contentLimit("Content hash entry limit exceeded");
1708
+ if (Reflect.ownKeys(entries).length !== entries.length + 1)
1709
+ invalidContent("Invalid content hash entry array");
1710
+ const snapshot = [];
1711
+ const paths = new SkillEntryPaths;
1712
+ let rawBytes = 0;
1713
+ for (let index = 0;index < entries.length; index++) {
1714
+ check();
1715
+ const descriptor = Object.getOwnPropertyDescriptor(entries, String(index));
1716
+ if (!descriptor || !("value" in descriptor))
1717
+ invalidContent("Invalid content hash entry array");
1718
+ const entry = contentRecord(descriptor.value, ["path", "bytes", "mode"]);
1719
+ if (typeof entry.path !== "string" || typeof entry.mode !== "number" || !Number.isInteger(entry.mode) || entry.mode < 0 || entry.mode > 511)
1720
+ invalidContent("Invalid regular-file content hash entry");
1721
+ paths.add(entry.path, limits.pathBytes, invalidContent, () => contentLimit("Content hash path limit exceeded"));
1722
+ if (!(entry.bytes instanceof Uint8Array) || !ArrayBuffer.isView(entry.bytes))
1723
+ invalidContent("Content hash entry requires bytes");
1724
+ const size = byteLengthOf.call(entry.bytes);
1725
+ if (!(bufferOf.call(entry.bytes) instanceof ArrayBuffer))
1726
+ invalidContent("Shared content hash bytes are unsupported");
1727
+ if (size > limits.fileBytes || rawBytes + size > limits.rawBytes)
1728
+ contentLimit("Content hash raw byte limit exceeded");
1729
+ if (entry.path === "skill.json" && size > limits.manifestBytes)
1730
+ contentLimit("Content hash manifest byte limit exceeded");
1731
+ rawBytes += size;
1732
+ const bytes = new Uint8Array(new ArrayBuffer(size));
1733
+ bytes.set(entry.bytes);
1734
+ snapshot.push({ path: entry.path, bytes, mode: entry.mode });
1735
+ }
1736
+ check();
1737
+ return snapshot;
1738
+ }
1739
+ function coveredContentPath(path) {
1740
+ const segments = path.split("/");
1741
+ if (!HASH_COVERAGE.includes(segments[0]))
1742
+ return false;
1743
+ return !segments.slice(1).some((segment, index) => excludedHashEntry(segment, index < segments.length - 2));
1744
+ }
1745
+ function boundedManifest(raw, maxDepth) {
1746
+ let parsed;
1747
+ try {
1748
+ parsed = JSON.parse(raw);
1749
+ } catch {
1750
+ return;
1751
+ }
1752
+ const pending = [{ value: parsed, depth: 1 }];
1753
+ while (pending.length) {
1754
+ const { value, depth } = pending.pop();
1755
+ if (!value || typeof value !== "object")
1756
+ continue;
1757
+ if (depth > maxDepth)
1758
+ contentLimit("Content hash manifest depth limit exceeded");
1759
+ for (const child of Object.values(value))
1760
+ pending.push({ value: child, depth: depth + 1 });
1761
+ }
1762
+ return parsed;
1763
+ }
1764
+ async function hashContentEntries(entries, options) {
1765
+ const { limits, signal } = contentOptions(options);
1766
+ const deadline = performance.now() + limits.timeoutMs;
1767
+ let terminal;
1768
+ const abort = () => {
1769
+ terminal ??= new ContentHashInputError("CONTENT_HASH_ABORTED", "Content hashing aborted");
1770
+ };
1771
+ const timer = setTimeout(() => {
1772
+ terminal ??= new ContentHashInputError("CONTENT_HASH_TIMEOUT", "Content hashing deadline exceeded");
1773
+ }, limits.timeoutMs);
1774
+ const check = () => {
1775
+ if (signal?.aborted)
1776
+ abort();
1777
+ if (terminal)
1778
+ throw terminal;
1779
+ if (performance.now() >= deadline)
1780
+ throw new ContentHashInputError("CONTENT_HASH_TIMEOUT", "Content hashing deadline exceeded");
1781
+ };
1782
+ try {
1783
+ signal?.addEventListener("abort", abort, { once: true });
1784
+ check();
1785
+ const snapshot = snapshotContentEntries(entries, limits, check);
1786
+ const normalized = [];
1787
+ let normalizedBytes = 0;
1788
+ let manifest;
1789
+ await new Promise((resolve2) => setImmediate(resolve2));
1790
+ for (const entry of snapshot) {
1791
+ check();
1792
+ if (!coveredContentPath(entry.path))
1793
+ continue;
1794
+ if (entry.path === "skill.json")
1795
+ manifest = boundedManifest(new TextDecoder().decode(entry.bytes), limits.manifestDepth);
1796
+ const file = normalizeBundleFile(entry.path, entry.bytes);
1797
+ check();
1798
+ if (file.content.byteLength > limits.normalizedFileBytes || normalizedBytes + file.content.byteLength > limits.normalizedBytes)
1799
+ contentLimit("Content hash normalized byte limit exceeded");
1800
+ normalizedBytes += file.content.byteLength;
1801
+ normalized.push(file);
1802
+ await new Promise((resolve2) => setImmediate(resolve2));
1803
+ }
1804
+ normalized.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
1805
+ check();
1806
+ return { hash: await hashBundleFilesCooperatively(normalized, check), manifest };
1807
+ } catch (error) {
1808
+ if (error instanceof ContentHashInputError)
1809
+ throw error;
1810
+ throw new ContentHashInputError("CONTENT_HASH_INVALID", "Invalid content hash input");
1811
+ } finally {
1812
+ clearTimeout(timer);
1813
+ signal?.removeEventListener("abort", abort);
1814
+ }
1815
+ }
1816
+ async function computeContentHashFromEntries(entries, options = {}) {
1817
+ return (await hashContentEntries(entries, options)).hash;
1818
+ }
1819
+ async function verifyContentHashFromEntries(entries, options = {}) {
1820
+ const { hash, manifest } = await hashContentEntries(entries, options);
1821
+ const provenance = manifest && typeof manifest === "object" && !Array.isArray(manifest) ? manifest.provenance : undefined;
1822
+ const value = provenance && typeof provenance === "object" && !Array.isArray(provenance) ? provenance.content_hash : undefined;
1823
+ if (value !== undefined && typeof value !== "string")
1824
+ invalidContent("Invalid content hash declaration");
1825
+ const declaredHash = value?.trim() || undefined;
1826
+ if (!declaredHash)
1827
+ return { declared: false, valid: false };
1828
+ if (!/^[a-f0-9]{64}$/.test(declaredHash))
1829
+ return { declared: true, valid: false, declaredHash };
1830
+ return { declared: true, valid: hash === declaredHash, declaredHash, computedHash: hash };
1831
+ }
1581
1832
  function verifyContentHash(skillPath, manifest) {
1582
1833
  const declaredHash = manifest?.provenance?.content_hash?.trim() || undefined;
1583
1834
  if (!declaredHash)
@@ -10254,11 +10505,174 @@ function primitiveHaystack(primitive) {
10254
10505
  function clone(value) {
10255
10506
  return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
10256
10507
  }
10257
- // src/lib/remote-workspace-selection.ts
10508
+ // src/lib/remote-invitations.ts
10509
+ class WorkspaceInvitationInputError extends Error {
10510
+ code = "INVITATION_INPUT_INVALID";
10511
+ constructor() {
10512
+ super("Provide only the documented invitation fields, exact lowercase IDs, expected generation and explicit confirmation. Issue and resend require your stable idempotency key.");
10513
+ this.name = "WorkspaceInvitationInputError";
10514
+ }
10515
+ }
10516
+ var invitationFailures = {
10517
+ INVALID_REQUEST: [400, "Invitation parameters were refused."],
10518
+ ACCOUNT_UNAVAILABLE: [403, "Account is unavailable."],
10519
+ INTERACTIVE_SESSION_REQUIRED: [403, "Fresh interactive sign-in is required."],
10520
+ WORKSPACE_ADMIN_REQUIRED: [403, "A current workspace owner or admin is required."],
10521
+ INVITATION_FORBIDDEN: [403, "Your current role cannot manage this invitation."],
10522
+ INVITATION_UNAVAILABLE: [404, "Invitation is unavailable for this account."],
10523
+ INVITATION_CHANGED: [409, "Invitation changed. Read its current generation before another action."],
10524
+ INVITATION_EXISTS: [409, "A pending invitation already exists. Read current invitations."],
10525
+ ALREADY_MEMBER: [409, "An active membership already exists. An invitation cannot change its role."],
10526
+ IDEMPOTENCY_CONFLICT: [409, "This request key was used for different invitation parameters. Reconcile the original request."],
10527
+ INVITATION_LIMIT: [429, "Invitation limit reached. Wait before issuing or resending."],
10528
+ INVITATION_BUSY: [503, "Invitation is busy. Read its state before another action."],
10529
+ INVITATION_DELIVERY_UNAVAILABLE: [503, "Invitation email delivery is unavailable."]
10530
+ };
10531
+
10532
+ class RemoteWorkspaceInvitationError extends Error {
10533
+ code;
10534
+ status;
10535
+ constructor(code) {
10536
+ super(invitationFailures[code][1]);
10537
+ this.code = code;
10538
+ this.name = "RemoteWorkspaceInvitationError";
10539
+ this.status = invitationFailures[code][0];
10540
+ }
10541
+ }
10542
+
10543
+ class RemoteWorkspaceInvitationUnconfirmedError extends Error {
10544
+ code = "INVITATION_UNCONFIRMED";
10545
+ constructor() {
10546
+ super("The invitation outcome is unconfirmed. Read current invitations or memberships. Reconcile issue/resend only with the same request key, parameters, server and membership; never generate a new key or retry automatically. Saved credentials are unchanged.");
10547
+ this.name = "RemoteWorkspaceInvitationUnconfirmedError";
10548
+ }
10549
+ }
10550
+
10551
+ class RemoteWorkspaceInvitationReadError extends Error {
10552
+ code = "INVITATION_READ_FAILED";
10553
+ constructor() {
10554
+ super("Unable to read a valid invitation result. Check the selected server, account, current membership and permissions.");
10555
+ this.name = "RemoteWorkspaceInvitationReadError";
10556
+ }
10557
+ }
10558
+ function invitationFailure(value, status) {
10559
+ if (!record(value) || typeof value.code !== "string" || !Object.hasOwn(invitationFailures, value.code))
10560
+ return null;
10561
+ const code = value.code;
10562
+ return invitationFailures[code][0] === status ? code : null;
10563
+ }
10258
10564
  var record = (v) => !!v && typeof v === "object" && !Array.isArray(v);
10259
10565
  var uuid = (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);
10260
- var text = (v, max = 1024) => typeof v === "string" && !!v.trim() && v.length <= max && !/[\p{Cc}\p{Cs}\u2028\u2029]/u.test(v);
10261
10566
  var role = (v) => typeof v === "string" && ["owner", "admin", "member", "viewer"].includes(v);
10567
+ var email = (v) => typeof v === "string" && v.length <= 254 && !/[\p{Cc}\p{Cs}\u2028\u2029\s]/u.test(v) && /^[^@]+@[^@]+\.[^@]+$/.test(v);
10568
+ var timestamp = (v) => typeof v === "string" && v.length <= 40 && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,6})?(?:Z|[+-]\d\d:\d\d)$/.test(v) && Number.isFinite(Date.parse(v));
10569
+ var inputFailure = () => {
10570
+ throw new WorkspaceInvitationInputError;
10571
+ };
10572
+ function invitationInput(action, input) {
10573
+ if (!record(input))
10574
+ return inputFailure();
10575
+ const keys = {
10576
+ list: ["after"],
10577
+ get: ["invitationId"],
10578
+ issue: ["email", "role", "idempotencyKey", "confirm"],
10579
+ resend: ["invitationId", "expectedGeneration", "idempotencyKey", "confirm"],
10580
+ revoke: ["invitationId", "expectedGeneration", "confirm"],
10581
+ accept: ["invitationId", "token", "confirm"]
10582
+ };
10583
+ if (Object.keys(input).some((key) => !keys[action].includes(key)) || action !== "list" && keys[action].some((key) => !Object.hasOwn(input, key)))
10584
+ return inputFailure();
10585
+ const value = { ...input };
10586
+ if (action === "list") {
10587
+ if (value.after !== undefined && !uuid(value.after))
10588
+ return inputFailure();
10589
+ }
10590
+ if (["get", "resend", "revoke", "accept"].includes(action) && !uuid(value.invitationId))
10591
+ return inputFailure();
10592
+ if (!["list", "get"].includes(action) && value.confirm !== true)
10593
+ return inputFailure();
10594
+ if (["issue", "resend"].includes(action) && !uuid(value.idempotencyKey))
10595
+ return inputFailure();
10596
+ if (action === "issue") {
10597
+ if (typeof value.email !== "string")
10598
+ return inputFailure();
10599
+ value.email = value.email.trim().toLowerCase();
10600
+ if (!email(value.email) || !role(value.role))
10601
+ return inputFailure();
10602
+ }
10603
+ if (["resend", "revoke"].includes(action) && (!Number.isInteger(value.expectedGeneration) || Number(value.expectedGeneration) < 1 || Number(value.expectedGeneration) > 10))
10604
+ return inputFailure();
10605
+ if (action === "accept" && (typeof value.token !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(value.token)))
10606
+ return inputFailure();
10607
+ return value;
10608
+ }
10609
+ function invitationRequest(action, input) {
10610
+ const base = "/api/v1/workspace/invitations", value = input;
10611
+ const { confirm: _confirm, invitationId: id, ...body } = value;
10612
+ if (action === "list")
10613
+ return { path: base + (value.after ? `?after=${value.after}` : ""), method: "GET" };
10614
+ if (action === "get")
10615
+ return { path: `${base}/${id}`, method: "GET" };
10616
+ if (action === "accept")
10617
+ return { path: "/api/v1/account/invitations/accept", method: "POST", body: JSON.stringify({ invitationId: id, token: value.token }) };
10618
+ return { path: action === "issue" ? base : `${base}/${id}${action === "resend" ? "/resend" : ""}`, method: action === "revoke" ? "DELETE" : "POST", body: JSON.stringify(body) };
10619
+ }
10620
+ function invalid() {
10621
+ throw new RemoteWorkspaceInvitationReadError;
10622
+ }
10623
+ function projection(v, organizationId) {
10624
+ if (!record(v) || !uuid(v.id) || v.organizationId !== organizationId || !email(v.email) || v.email !== v.email.trim().toLowerCase() || !role(v.role) || !Number.isInteger(v.generation) || Number(v.generation) < 1 || Number(v.generation) > 10 || typeof v.status !== "string" || !["pending", "expired", "accepted", "revoked"].includes(v.status) || !timestamp(v.expiresAt) || !timestamp(v.createdAt) || !record(v.delivery) || typeof v.delivery.state !== "string" || !["queued", "sending", "uncertain", "provider_accepted", "failed", "cancelled"].includes(v.delivery.state) || !Number.isInteger(v.delivery.attempts) || Number(v.delivery.attempts) < 0 || Number(v.delivery.attempts) > 5)
10625
+ return invalid();
10626
+ return {
10627
+ id: v.id,
10628
+ organizationId,
10629
+ email: v.email,
10630
+ role: v.role,
10631
+ generation: Number(v.generation),
10632
+ status: v.status,
10633
+ expiresAt: v.expiresAt,
10634
+ createdAt: v.createdAt,
10635
+ delivery: { state: v.delivery.state, attempts: Number(v.delivery.attempts) }
10636
+ };
10637
+ }
10638
+ function parseInvitationResult(action, value, input, organizationId) {
10639
+ if (!record(value))
10640
+ return invalid();
10641
+ const request = input;
10642
+ if (action === "accept") {
10643
+ if (!uuid(value.organizationId) || !uuid(value.membershipId) || value.accepted !== true || typeof value.changed !== "boolean")
10644
+ return invalid();
10645
+ return { organizationId: value.organizationId, membershipId: value.membershipId, accepted: true, changed: value.changed };
10646
+ }
10647
+ if (action === "list") {
10648
+ if (value.organizationId !== organizationId || !Array.isArray(value.invitations) || value.invitations.length > 50 || value.nextCursor !== null && !uuid(value.nextCursor))
10649
+ return invalid();
10650
+ const invitations = value.invitations.map((v) => projection(v, organizationId));
10651
+ if (invitations.some((v, n) => v.id <= String(n ? invitations[n - 1].id : request.after ?? "")) || value.nextCursor !== null && (invitations.length !== 50 || value.nextCursor !== invitations.at(-1)?.id))
10652
+ return invalid();
10653
+ return { organizationId, invitations, nextCursor: value.nextCursor };
10654
+ }
10655
+ const invitation = projection(value.invitation, organizationId);
10656
+ if (action !== "issue" && invitation.id !== request.invitationId)
10657
+ return invalid();
10658
+ if (action === "get")
10659
+ return { invitation };
10660
+ if (typeof value.changed !== "boolean")
10661
+ return invalid();
10662
+ if (action === "issue" && (invitation.email !== request.email || invitation.role !== request.role || value.changed && invitation.generation !== 1))
10663
+ return invalid();
10664
+ if (action === "resend" && (value.changed ? invitation.generation !== Number(request.expectedGeneration) + 1 : invitation.generation <= Number(request.expectedGeneration)))
10665
+ return invalid();
10666
+ if (action === "revoke" && (invitation.status !== "revoked" || invitation.generation < Number(request.expectedGeneration) || value.changed && invitation.generation !== request.expectedGeneration))
10667
+ return invalid();
10668
+ return { invitation, changed: value.changed };
10669
+ }
10670
+
10671
+ // src/lib/remote-workspace-selection.ts
10672
+ var record2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
10673
+ var uuid2 = (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);
10674
+ var text = (v, max = 1024) => typeof v === "string" && !!v.trim() && v.length <= max && !/[\p{Cc}\p{Cs}\u2028\u2029]/u.test(v);
10675
+ var role2 = (v) => typeof v === "string" && ["owner", "admin", "member", "viewer"].includes(v);
10262
10676
  var invalidWorkspaceResult = "The server returned an invalid workspace selection result.";
10263
10677
 
10264
10678
  class WorkspaceContextInputError extends Error {
@@ -10275,48 +10689,48 @@ class WorkspaceIdentityMismatchError extends Error {
10275
10689
  }
10276
10690
  }
10277
10691
  function workspaceExpectedUserId(value) {
10278
- if (!uuid(value))
10692
+ if (!uuid2(value))
10279
10693
  throw new WorkspaceContextInputError;
10280
10694
  return value;
10281
10695
  }
10282
10696
  function workspaceContext(value) {
10283
- if (!record(value) || Object.keys(value).sort().join(",") !== "membershipId,userId" || !uuid(value.userId) || !uuid(value.membershipId))
10697
+ if (!record2(value) || Object.keys(value).sort().join(",") !== "membershipId,userId" || !uuid2(value.userId) || !uuid2(value.membershipId))
10284
10698
  throw new WorkspaceContextInputError;
10285
10699
  return { userId: value.userId, membershipId: value.membershipId };
10286
10700
  }
10287
- function invalid() {
10701
+ function invalid2() {
10288
10702
  throw new Error(invalidWorkspaceResult);
10289
10703
  }
10290
10704
  function organization(v) {
10291
- if (!record(v) || !uuid(v.id) || !text(v.slug) || !text(v.name))
10292
- return invalid();
10705
+ if (!record2(v) || !uuid2(v.id) || !text(v.slug) || !text(v.name))
10706
+ return invalid2();
10293
10707
  return { id: v.id, slug: v.slug, name: v.name };
10294
10708
  }
10295
10709
  function parseAccountWorkspaces(value) {
10296
- if (!record(value) || !Array.isArray(value.workspaces) || !value.workspaces.length || value.workspaces.length > 1000)
10297
- return invalid();
10710
+ if (!record2(value) || !Array.isArray(value.workspaces) || !value.workspaces.length || value.workspaces.length > 1000)
10711
+ return invalid2();
10298
10712
  const workspaces = value.workspaces.map((v) => {
10299
- if (!record(v) || !uuid(v.membershipId) || !role(v.role) || typeof v.current !== "boolean")
10300
- return invalid();
10713
+ if (!record2(v) || !uuid2(v.membershipId) || !role2(v.role) || typeof v.current !== "boolean")
10714
+ return invalid2();
10301
10715
  return { membershipId: v.membershipId, organization: organization(v.organization), role: v.role, current: v.current };
10302
10716
  });
10303
10717
  if (workspaces.filter((w) => w.current).length !== 1 || new Set(workspaces.map((w) => w.membershipId)).size !== workspaces.length || new Set(workspaces.map((w) => w.organization.id)).size !== workspaces.length)
10304
- return invalid();
10718
+ return invalid2();
10305
10719
  return { workspaces };
10306
10720
  }
10307
10721
  function parseWorkspaceIdentity(value, expectedUserId) {
10308
- if (!record(value))
10309
- return invalid();
10722
+ if (!record2(value))
10723
+ return invalid2();
10310
10724
  const user = value.user;
10311
- if (!record(user) || !uuid(user.id) || !uuid(user.membershipId) || !text(user.email, 320) || !(user.displayName === null || text(user.displayName)) || !role(user.role))
10312
- return invalid();
10725
+ if (!record2(user) || !uuid2(user.id) || !uuid2(user.membershipId) || !text(user.email, 320) || !(user.displayName === null || text(user.displayName)) || !role2(user.role))
10726
+ return invalid2();
10313
10727
  if (user.id !== expectedUserId)
10314
10728
  throw new WorkspaceIdentityMismatchError;
10315
10729
  return { user: { id: user.id, membershipId: user.membershipId, email: user.email, displayName: user.displayName, role: user.role }, organization: organization(value.organization) };
10316
10730
  }
10317
10731
  function sessionToken(value) {
10318
10732
  if (typeof value !== "string" || !value || value.length > 8192 || /[^\x21-\x7e]/.test(value) || value.startsWith("sk_"))
10319
- return invalid();
10733
+ return invalid2();
10320
10734
  return value;
10321
10735
  }
10322
10736
  function parseWorkspaceSession(value, expected) {
@@ -10326,9 +10740,9 @@ function parseWorkspaceSession(value, expected) {
10326
10740
  return { token: sessionToken(value.token), ...identity };
10327
10741
  }
10328
10742
  function parseWorkspaceLogin(value, expectedUserId) {
10329
- const user = record(value) && value.user;
10330
- if (!record(value) || !record(user) || !uuid(user.id))
10331
- return invalid();
10743
+ const user = record2(value) && value.user;
10744
+ if (!record2(value) || !record2(user) || !uuid2(user.id))
10745
+ return invalid2();
10332
10746
  if (expectedUserId !== undefined && user.id !== expectedUserId)
10333
10747
  throw new WorkspaceIdentityMismatchError;
10334
10748
  return { token: sessionToken(value.token), userId: user.id };
@@ -10342,18 +10756,18 @@ var workspaceSelectionFailures = {
10342
10756
  WORKSPACE_BUSY: [503, "Workspace is busy. Refresh before retrying."]
10343
10757
  };
10344
10758
  function workspaceSelectionFailure(value, status) {
10345
- if (!record(value) || typeof value.code !== "string" || !Object.hasOwn(workspaceSelectionFailures, value.code))
10759
+ if (!record2(value) || typeof value.code !== "string" || !Object.hasOwn(workspaceSelectionFailures, value.code))
10346
10760
  return null;
10347
10761
  const code = value.code;
10348
10762
  return workspaceSelectionFailures[code][0] === status ? code : null;
10349
10763
  }
10350
10764
 
10351
10765
  // src/lib/remote-workspace.ts
10352
- var record2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
10766
+ var record3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
10353
10767
  var cursor = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,512}$/.test(value);
10354
- var uuid2 = (value) => typeof value === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value);
10768
+ var uuid3 = (value) => typeof value === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value);
10355
10769
  function workspaceMembersQuery(options = {}) {
10356
- if (!record2(options) || Object.keys(options).some((key) => key !== "limit" && key !== "cursor") || options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 100) || options.cursor !== undefined && !cursor(options.cursor))
10770
+ if (!record3(options) || Object.keys(options).some((key) => key !== "limit" && key !== "cursor") || options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 100) || options.cursor !== undefined && !cursor(options.cursor))
10357
10771
  throw new Error("Use a roster limit from 1 to 100 and an unchanged continuation cursor.");
10358
10772
  const query = new URLSearchParams;
10359
10773
  if (options.limit !== undefined)
@@ -10362,14 +10776,14 @@ function workspaceMembersQuery(options = {}) {
10362
10776
  query.set("cursor", options.cursor);
10363
10777
  return query.size ? `?${query}` : "";
10364
10778
  }
10365
- function timestamp(value) {
10779
+ function timestamp2(value) {
10366
10780
  if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$/.test(value))
10367
10781
  return false;
10368
10782
  const time = Date.parse(value);
10369
10783
  return Number.isFinite(time) && new Date(time).toISOString().slice(0, 23) === value.slice(0, 23);
10370
10784
  }
10371
10785
  function parseMember(row, fail) {
10372
- if (!record2(row) || !uuid2(row.membershipId) || !uuid2(row.userId) || typeof row.email !== "string" || !row.email || !(row.displayName === null || typeof row.displayName === "string") || !isRole(row.role) || !timestamp(row.createdAt))
10786
+ if (!record3(row) || !uuid3(row.membershipId) || !uuid3(row.userId) || typeof row.email !== "string" || !row.email || !(row.displayName === null || typeof row.displayName === "string") || !isRole(row.role) || !timestamp2(row.createdAt))
10373
10787
  return fail();
10374
10788
  return {
10375
10789
  membershipId: row.membershipId,
@@ -10389,12 +10803,12 @@ class WorkspaceMemberInputError extends Error {
10389
10803
  }
10390
10804
  }
10391
10805
  function mutationInput(membershipId, input, roleChange) {
10392
- if (typeof membershipId !== "string" || !uuid2(membershipId) || membershipId !== membershipId.toLowerCase() || !record2(input) || Object.keys(input).sort().join(",") !== (roleChange ? "expectedRole,role" : "expectedRole"))
10806
+ if (typeof membershipId !== "string" || !uuid3(membershipId) || membershipId !== membershipId.toLowerCase() || !record3(input) || Object.keys(input).sort().join(",") !== (roleChange ? "expectedRole,role" : "expectedRole"))
10393
10807
  throw new WorkspaceMemberInputError;
10394
- const expectedRole = input.expectedRole, role2 = roleChange ? input.role : undefined;
10395
- if (!isRole(expectedRole) || roleChange && !isRole(role2))
10808
+ const expectedRole = input.expectedRole, role3 = roleChange ? input.role : undefined;
10809
+ if (!isRole(expectedRole) || roleChange && !isRole(role3))
10396
10810
  throw new WorkspaceMemberInputError;
10397
- return { membershipId, role: role2, expectedRole };
10811
+ return { membershipId, role: role3, expectedRole };
10398
10812
  }
10399
10813
  function workspaceMemberRoleInput(membershipId, input) {
10400
10814
  const value = mutationInput(membershipId, input, true);
@@ -10405,19 +10819,19 @@ function workspaceMemberRemovalInput(membershipId, input) {
10405
10819
  return { membershipId: value.membershipId, body: { expectedRole: value.expectedRole } };
10406
10820
  }
10407
10821
  var invalidMemberResult = "The server returned an invalid workspace member result. Refresh the roster before another action.";
10408
- function parseWorkspaceMemberRoleResult(value, membershipId, role2) {
10822
+ function parseWorkspaceMemberRoleResult(value, membershipId, role3) {
10409
10823
  const fail = () => {
10410
10824
  throw new Error(invalidMemberResult);
10411
10825
  };
10412
- if (!record2(value) || !uuid2(value.organizationId) || typeof value.changed !== "boolean")
10826
+ if (!record3(value) || !uuid3(value.organizationId) || typeof value.changed !== "boolean")
10413
10827
  return fail();
10414
10828
  const member = parseMember(value.member, fail);
10415
- if (member.membershipId !== membershipId || member.role !== role2)
10829
+ if (member.membershipId !== membershipId || member.role !== role3)
10416
10830
  return fail();
10417
10831
  return { organizationId: value.organizationId, member, changed: value.changed };
10418
10832
  }
10419
10833
  function parseWorkspaceMemberRemovalResult(value, membershipId) {
10420
- if (!record2(value) || !uuid2(value.organizationId) || value.membershipId !== membershipId || value.removed !== true || typeof value.alreadyRemoved !== "boolean")
10834
+ if (!record3(value) || !uuid3(value.organizationId) || value.membershipId !== membershipId || value.removed !== true || typeof value.alreadyRemoved !== "boolean")
10421
10835
  throw new Error(invalidMemberResult);
10422
10836
  return { organizationId: value.organizationId, membershipId, removed: true, alreadyRemoved: value.alreadyRemoved };
10423
10837
  }
@@ -10434,7 +10848,7 @@ var workspaceMemberFailures = {
10434
10848
  MEMBERSHIP_BUSY: [503, "Membership is busy. Refresh the roster before another action."]
10435
10849
  };
10436
10850
  function workspaceMemberFailure(value, status) {
10437
- if (!record2(value) || typeof value.code !== "string" || !Object.hasOwn(workspaceMemberFailures, value.code))
10851
+ if (!record3(value) || typeof value.code !== "string" || !Object.hasOwn(workspaceMemberFailures, value.code))
10438
10852
  return null;
10439
10853
  const code = value.code;
10440
10854
  return workspaceMemberFailures[code][0] === status ? code : null;
@@ -10443,7 +10857,7 @@ function parseWorkspaceMembersPage(value) {
10443
10857
  const fail = () => {
10444
10858
  throw new Error("The server returned an invalid workspace roster.");
10445
10859
  };
10446
- if (!record2(value) || !uuid2(value.organizationId) || !Array.isArray(value.members) || value.members.length > 100 || !(value.nextCursor === null || cursor(value.nextCursor)) || !value.members.length && value.nextCursor !== null)
10860
+ if (!record3(value) || !uuid3(value.organizationId) || !Array.isArray(value.members) || value.members.length > 100 || !(value.nextCursor === null || cursor(value.nextCursor)) || !value.members.length && value.nextCursor !== null)
10447
10861
  return fail();
10448
10862
  const members = value.members.map((row) => parseMember(row, fail));
10449
10863
  if (new Set(members.map((row) => row.membershipId)).size !== members.length)
@@ -10517,44 +10931,44 @@ function getApiUrl(action, env = process.env, options = {}) {
10517
10931
  // src/lib/remote-run-contract.ts
10518
10932
  var REMOTE_SKILL_RUN_CONTRACT_VERSION = 1;
10519
10933
  function normalizeRemoteSkillRunContract(payload, fallbackSkill) {
10520
- const record3 = isRecord3(payload) ? payload : {};
10934
+ const record4 = isRecord3(payload) ? payload : {};
10521
10935
  return {
10522
10936
  contractVersion: REMOTE_SKILL_RUN_CONTRACT_VERSION,
10523
- ...pickString(record3, "id"),
10524
- skill: pickStringValue(record3, "skill") ?? fallbackSkill,
10525
- ...pickString(record3, "requestedSlug"),
10526
- ...pickString(record3, "status"),
10527
- ...pickNumber(record3, "exitCode"),
10528
- ...pickString(record3, "correlationId"),
10529
- ...pickString(record3, "createdAt"),
10530
- ...pickString(record3, "startedAt"),
10531
- ...pickString(record3, "completedAt"),
10532
- ...pickNumber(record3, "durationMs"),
10533
- ...pickString(record3, "outputType"),
10534
- ...hasOwn(record3, "outputPreview") ? { outputPreview: record3.outputPreview } : {},
10535
- ...pickString(record3, "errorCode"),
10536
- ...pickString(record3, "errorMessage"),
10537
- ...pickString(record3, "error"),
10538
- ...pickString(record3, "code"),
10539
- ...hasOwn(record3, "details") ? { details: record3.details } : {}
10937
+ ...pickString(record4, "id"),
10938
+ skill: pickStringValue(record4, "skill") ?? fallbackSkill,
10939
+ ...pickString(record4, "requestedSlug"),
10940
+ ...pickString(record4, "status"),
10941
+ ...pickNumber(record4, "exitCode"),
10942
+ ...pickString(record4, "correlationId"),
10943
+ ...pickString(record4, "createdAt"),
10944
+ ...pickString(record4, "startedAt"),
10945
+ ...pickString(record4, "completedAt"),
10946
+ ...pickNumber(record4, "durationMs"),
10947
+ ...pickString(record4, "outputType"),
10948
+ ...hasOwn(record4, "outputPreview") ? { outputPreview: record4.outputPreview } : {},
10949
+ ...pickString(record4, "errorCode"),
10950
+ ...pickString(record4, "errorMessage"),
10951
+ ...pickString(record4, "error"),
10952
+ ...pickString(record4, "code"),
10953
+ ...hasOwn(record4, "details") ? { details: record4.details } : {}
10540
10954
  };
10541
10955
  }
10542
10956
  function isRecord3(value) {
10543
10957
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
10544
10958
  }
10545
- function hasOwn(record3, key) {
10546
- return Object.prototype.hasOwnProperty.call(record3, key);
10959
+ function hasOwn(record4, key) {
10960
+ return Object.prototype.hasOwnProperty.call(record4, key);
10547
10961
  }
10548
- function pickString(record3, key) {
10549
- const value = pickStringValue(record3, key);
10962
+ function pickString(record4, key) {
10963
+ const value = pickStringValue(record4, key);
10550
10964
  return value === undefined ? {} : { [key]: value };
10551
10965
  }
10552
- function pickStringValue(record3, key) {
10553
- const value = record3[key];
10966
+ function pickStringValue(record4, key) {
10967
+ const value = record4[key];
10554
10968
  return typeof value === "string" ? value : undefined;
10555
10969
  }
10556
- function pickNumber(record3, key) {
10557
- const value = record3[key];
10970
+ function pickNumber(record4, key) {
10971
+ const value = record4[key];
10558
10972
  return typeof value === "number" && Number.isFinite(value) ? { [key]: value } : {};
10559
10973
  }
10560
10974
 
@@ -11032,6 +11446,53 @@ class RemoteSkillsClient {
11032
11446
  }
11033
11447
  return parseWorkspaceLeaveResult(body, captured.context.membershipId, identity.organization.id);
11034
11448
  }
11449
+ listWorkspaceInvitations(context, options = {}) {
11450
+ return this.requestWorkspaceInvitation(context, "list", options);
11451
+ }
11452
+ getWorkspaceInvitation(context, invitationId) {
11453
+ return this.requestWorkspaceInvitation(context, "get", { invitationId });
11454
+ }
11455
+ issueWorkspaceInvitation(context, input) {
11456
+ return this.requestWorkspaceInvitation(context, "issue", input);
11457
+ }
11458
+ resendWorkspaceInvitation(context, invitationId, input) {
11459
+ return this.requestWorkspaceInvitation(context, "resend", { ...input, invitationId });
11460
+ }
11461
+ revokeWorkspaceInvitation(context, invitationId, input) {
11462
+ return this.requestWorkspaceInvitation(context, "revoke", { ...input, invitationId });
11463
+ }
11464
+ acceptWorkspaceInvitation(context, invitationId, input) {
11465
+ return this.requestWorkspaceInvitation(context, "accept", { ...input, invitationId });
11466
+ }
11467
+ async requestWorkspaceInvitation(context, action, input) {
11468
+ const target = workspaceContext(context), captured = invitationInput(action, input);
11469
+ const connection = new RemoteSkillsClient(this.apiKey, this.apiUrl);
11470
+ const identityValue = await connection.requestWorkspaceSelection("/api/auth/whoami");
11471
+ if (!identityValue || typeof identityValue !== "object" || identityValue.authMethod !== "jwt")
11472
+ throw new RemoteWorkspaceInvitationError("INTERACTIVE_SESSION_REQUIRED");
11473
+ const identity = parseWorkspaceIdentity(identityValue, target.userId);
11474
+ if (identity.user.membershipId !== target.membershipId)
11475
+ throw new WorkspaceIdentityMismatchError;
11476
+ const request = invitationRequest(action, captured), read = action === "list" || action === "get";
11477
+ let response, value;
11478
+ try {
11479
+ response = await connection.request(request.path, { method: request.method, ...request.body ? { body: request.body } : {}, credentials: "omit" });
11480
+ value = JSON.parse(new TextDecoder().decode(await readBoundedResponse(response, response.ok ? 64 * 1024 : 4096)));
11481
+ } catch {
11482
+ throw read ? new RemoteWorkspaceInvitationReadError : new RemoteWorkspaceInvitationUnconfirmedError;
11483
+ }
11484
+ if (!response.ok) {
11485
+ const code = invitationFailure(value, response.status);
11486
+ if (code)
11487
+ throw new RemoteWorkspaceInvitationError(code);
11488
+ throw read ? new RemoteWorkspaceInvitationReadError : new RemoteWorkspaceInvitationUnconfirmedError;
11489
+ }
11490
+ try {
11491
+ return parseInvitationResult(action, value, captured, identity.organization.id);
11492
+ } catch {
11493
+ throw read ? new RemoteWorkspaceInvitationReadError : new RemoteWorkspaceInvitationUnconfirmedError;
11494
+ }
11495
+ }
11035
11496
  async listApiKeys() {
11036
11497
  return this.arrayResponse("/api/auth/keys");
11037
11498
  }
@@ -11282,13 +11743,13 @@ class RemoteSkillsClient {
11282
11743
  return normalizeUpdatedSincePage(await response.json());
11283
11744
  }
11284
11745
  }
11285
- function requireOptionalString(record3, field) {
11286
- if (record3[field] === undefined)
11746
+ function requireOptionalString(record4, field) {
11747
+ if (record4[field] === undefined)
11287
11748
  return;
11288
- if (typeof record3[field] !== "string") {
11749
+ if (typeof record4[field] !== "string") {
11289
11750
  throw new Error(`Remote payload did not match the expected contract (${field} must be a string when present)`);
11290
11751
  }
11291
- return record3[field];
11752
+ return record4[field];
11292
11753
  }
11293
11754
  var INVALID_SKILL_VERSION_RESPONSE = "Remote skill version payload did not match the expected contract.";
11294
11755
  function isVersionRecord(value) {
@@ -11311,19 +11772,19 @@ function normalizePin(entry) {
11311
11772
  if (!entry || typeof entry !== "object") {
11312
11773
  throw new Error("Remote pin payload did not match the expected contract (expected an object)");
11313
11774
  }
11314
- const record3 = entry;
11315
- const slug = typeof record3.slug === "string" && record3.slug.trim() ? record3.slug.trim() : undefined;
11775
+ const record4 = entry;
11776
+ const slug = typeof record4.slug === "string" && record4.slug.trim() ? record4.slug.trim() : undefined;
11316
11777
  if (!slug) {
11317
11778
  throw new Error("Remote pin payload did not match the expected contract (missing slug)");
11318
11779
  }
11319
11780
  let metadata;
11320
- if (record3.metadata !== undefined) {
11321
- if (!record3.metadata || typeof record3.metadata !== "object" || Array.isArray(record3.metadata)) {
11781
+ if (record4.metadata !== undefined) {
11782
+ if (!record4.metadata || typeof record4.metadata !== "object" || Array.isArray(record4.metadata)) {
11322
11783
  throw new Error("Remote pin payload did not match the expected contract (metadata must be a JSON object when present)");
11323
11784
  }
11324
- metadata = record3.metadata;
11785
+ metadata = record4.metadata;
11325
11786
  }
11326
- const pinnedAt = requireOptionalString(record3, "pinnedAt");
11787
+ const pinnedAt = requireOptionalString(record4, "pinnedAt");
11327
11788
  return {
11328
11789
  slug,
11329
11790
  ...pinnedAt !== undefined ? { pinnedAt } : {},
@@ -11340,16 +11801,16 @@ function normalizeSkillSummary(entry) {
11340
11801
  if (!entry || typeof entry !== "object") {
11341
11802
  throw new Error("Remote skill payload did not match the expected contract (expected an object)");
11342
11803
  }
11343
- const record3 = entry;
11344
- const slug = typeof record3.slug === "string" && record3.slug.trim() ? record3.slug.trim() : undefined;
11804
+ const record4 = entry;
11805
+ const slug = typeof record4.slug === "string" && record4.slug.trim() ? record4.slug.trim() : undefined;
11345
11806
  if (!slug) {
11346
11807
  throw new Error("Remote skill payload did not match the expected contract (missing slug)");
11347
11808
  }
11348
11809
  return {
11349
11810
  slug,
11350
- ...requireOptionalString(record3, "name") !== undefined ? { name: requireOptionalString(record3, "name") } : {},
11351
- ...requireOptionalString(record3, "version") !== undefined ? { version: requireOptionalString(record3, "version") } : {},
11352
- ...requireOptionalString(record3, "updatedAt") !== undefined ? { updatedAt: requireOptionalString(record3, "updatedAt") } : {}
11811
+ ...requireOptionalString(record4, "name") !== undefined ? { name: requireOptionalString(record4, "name") } : {},
11812
+ ...requireOptionalString(record4, "version") !== undefined ? { version: requireOptionalString(record4, "version") } : {},
11813
+ ...requireOptionalString(record4, "updatedAt") !== undefined ? { updatedAt: requireOptionalString(record4, "updatedAt") } : {}
11353
11814
  };
11354
11815
  }
11355
11816
  function normalizeSkillSummaryList(payload) {
@@ -11402,12 +11863,12 @@ function normalizeUpdatedSincePage(payload) {
11402
11863
  if (!payload || typeof payload !== "object") {
11403
11864
  throw new Error("Updated-since payload did not match the expected contract (expected an object)");
11404
11865
  }
11405
- const record3 = payload;
11406
- if (!Array.isArray(record3.skills)) {
11866
+ const record4 = payload;
11867
+ if (!Array.isArray(record4.skills)) {
11407
11868
  throw new Error("Updated-since payload did not match the expected contract (missing skills array)");
11408
11869
  }
11409
- const skills = record3.skills.map(normalizeSkillSummary);
11410
- const nextCursor = record3.nextCursor === undefined || record3.nextCursor === null ? null : record3.nextCursor;
11870
+ const skills = record4.skills.map(normalizeSkillSummary);
11871
+ const nextCursor = record4.nextCursor === undefined || record4.nextCursor === null ? null : record4.nextCursor;
11411
11872
  if (nextCursor !== null && typeof nextCursor !== "string") {
11412
11873
  throw new Error("Updated-since payload did not match the expected contract (nextCursor must be a string or absent)");
11413
11874
  }
@@ -11621,10 +12082,6 @@ function recordScheduleRun(id, status, targetDir) {
11621
12082
  schedule.nextRun = getNextRun(schedule.cron, now)?.toISOString();
11622
12083
  saveSchedules(data, targetDir);
11623
12084
  }
11624
- // src/lib/pull.ts
11625
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, readFileSync as readFileSync15, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
11626
- import { dirname as dirname6, join as join17 } from "path";
11627
-
11628
12085
  // src/lib/revision.ts
11629
12086
  import { createHash as createHash4 } from "crypto";
11630
12087
  var REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
@@ -11644,14 +12101,18 @@ function revisionIdOf(content) {
11644
12101
  });
11645
12102
  return createHash4("sha256").update(canonical).digest("hex");
11646
12103
  }
11647
- function revisionIdOfRecord(record3) {
11648
- return revisionIdOf(record3);
12104
+ function revisionIdOfRecord(record4) {
12105
+ return revisionIdOf(record4);
11649
12106
  }
12107
+ // src/lib/pull.ts
12108
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, readFileSync as readFileSync15, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
12109
+ import { dirname as dirname6, join as join17 } from "path";
11650
12110
 
11651
12111
  // src/lib/skill-bundle.ts
11652
12112
  import { createHash as createHash5 } from "crypto";
11653
12113
  import { readFileSync as readFileSync14, readdirSync as readdirSync9, statSync as statSync9 } from "fs";
11654
12114
  import { join as join16, relative as relative3 } from "path";
12115
+ import { createGunzip } from "zlib";
11655
12116
  var BLOCK = 512;
11656
12117
  var ANY_SEGMENT_EXCLUDES = new Set([
11657
12118
  ".git",
@@ -11974,6 +12435,227 @@ function concat(chunks) {
11974
12435
  }
11975
12436
  return merged;
11976
12437
  }
12438
+ var SKILL_BUNDLE_INSPECTION_LIMITS = Object.freeze({
12439
+ compressedBytes: 16 * 1024 * 1024,
12440
+ decompressedBytes: 64 * 1024 * 1024,
12441
+ entries: 1024,
12442
+ fileBytes: 16 * 1024 * 1024,
12443
+ pathBytes: 100,
12444
+ timeoutMs: 5000
12445
+ });
12446
+
12447
+ class SkillBundleInspectionError extends Error {
12448
+ code;
12449
+ constructor(code, message) {
12450
+ super(message);
12451
+ this.code = code;
12452
+ this.name = "SkillBundleInspectionError";
12453
+ }
12454
+ }
12455
+ function invalidBundle(message) {
12456
+ throw new SkillBundleInspectionError("BUNDLE_INVALID", message);
12457
+ }
12458
+ function inspectionLimits(options) {
12459
+ const limits = { ...SKILL_BUNDLE_INSPECTION_LIMITS };
12460
+ for (const key of Object.keys(options.limits ?? {})) {
12461
+ if (!Object.hasOwn(limits, key))
12462
+ throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Unknown bundle limit");
12463
+ const field = key;
12464
+ const value = options.limits[field];
12465
+ if (!Number.isSafeInteger(value) || value <= 0 || value > limits[field]) {
12466
+ throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle limits must be positive integers within the hard ceilings");
12467
+ }
12468
+ limits[field] = value;
12469
+ }
12470
+ return limits;
12471
+ }
12472
+ async function inspectSkillBundle(bundle, options = {}) {
12473
+ const signal = options.signal;
12474
+ const limits = inspectionLimits(options);
12475
+ const deadline = performance.now() + limits.timeoutMs;
12476
+ const check = () => {
12477
+ if (signal?.aborted)
12478
+ throw new SkillBundleInspectionError("BUNDLE_ABORTED", "Bundle inspection aborted");
12479
+ if (performance.now() >= deadline)
12480
+ throw new SkillBundleInspectionError("BUNDLE_TIMEOUT", "Bundle inspection deadline exceeded");
12481
+ };
12482
+ check();
12483
+ if (bundle.byteLength > limits.compressedBytes)
12484
+ throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Compressed bundle exceeds byte limit");
12485
+ const snapshot = ownBytes(bundle);
12486
+ check();
12487
+ const sha2562 = sha256Hex(snapshot);
12488
+ check();
12489
+ const parser = new BoundedTarReader(limits, check);
12490
+ const streamOptions = { chunkSize: 16 * 1024, highWaterMark: 16 * 1024 };
12491
+ const decoder = createGunzip(streamOptions);
12492
+ let terminalError;
12493
+ const stop = (code) => {
12494
+ terminalError ??= new SkillBundleInspectionError(code, code === "BUNDLE_ABORTED" ? "Bundle inspection aborted" : "Bundle inspection deadline exceeded");
12495
+ decoder.destroy(terminalError);
12496
+ };
12497
+ const onAbort = () => stop("BUNDLE_ABORTED");
12498
+ const timer = setTimeout(() => stop("BUNDLE_TIMEOUT"), Math.max(1, deadline - performance.now()));
12499
+ signal?.addEventListener("abort", onAbort, { once: true });
12500
+ let decompressedByteSize = 0;
12501
+ let bytesSinceYield = 0;
12502
+ try {
12503
+ check();
12504
+ decoder.end(snapshot);
12505
+ for await (const chunk of decoder) {
12506
+ check();
12507
+ decompressedByteSize += chunk.byteLength;
12508
+ if (decompressedByteSize > limits.decompressedBytes)
12509
+ throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Decompressed bundle exceeds byte limit");
12510
+ parser.push(chunk);
12511
+ bytesSinceYield += chunk.byteLength;
12512
+ if (bytesSinceYield >= 256 * 1024) {
12513
+ await new Promise((resolve2) => setTimeout(resolve2, 0));
12514
+ bytesSinceYield = 0;
12515
+ check();
12516
+ }
12517
+ }
12518
+ check();
12519
+ const entries = parser.finish();
12520
+ return {
12521
+ entries,
12522
+ sha256: sha2562,
12523
+ compressedByteSize: snapshot.byteLength,
12524
+ decompressedByteSize,
12525
+ unpackedByteSize: parser.fileBytes,
12526
+ fileCount: entries.length
12527
+ };
12528
+ } catch (error) {
12529
+ if (terminalError)
12530
+ throw terminalError;
12531
+ if (error instanceof SkillBundleInspectionError)
12532
+ throw error;
12533
+ throw new SkillBundleInspectionError("BUNDLE_INVALID", "Invalid or truncated gzip bundle");
12534
+ } finally {
12535
+ clearTimeout(timer);
12536
+ signal?.removeEventListener("abort", onAbort);
12537
+ decoder.destroy();
12538
+ }
12539
+ }
12540
+
12541
+ class BoundedTarReader {
12542
+ limits;
12543
+ check;
12544
+ header = new Uint8Array(BLOCK);
12545
+ headerOffset = 0;
12546
+ pending;
12547
+ bodyOffset = 0;
12548
+ padding = 0;
12549
+ zeroBlocks = 0;
12550
+ entries = [];
12551
+ paths = new SkillEntryPaths;
12552
+ fileBytes = 0;
12553
+ constructor(limits, check) {
12554
+ this.limits = limits;
12555
+ this.check = check;
12556
+ }
12557
+ push(chunk) {
12558
+ let offset = 0;
12559
+ while (offset < chunk.byteLength) {
12560
+ this.check();
12561
+ if (this.pending) {
12562
+ const count = Math.min(this.pending.bytes.byteLength - this.bodyOffset, chunk.byteLength - offset);
12563
+ this.pending.bytes.set(chunk.subarray(offset, offset + count), this.bodyOffset);
12564
+ offset += count;
12565
+ this.bodyOffset += count;
12566
+ if (this.bodyOffset === this.pending.bytes.byteLength) {
12567
+ this.entries.push(this.pending);
12568
+ this.pending = undefined;
12569
+ }
12570
+ } else if (this.padding) {
12571
+ const count = Math.min(this.padding, chunk.byteLength - offset);
12572
+ if (chunk.subarray(offset, offset + count).some((byte) => byte !== 0))
12573
+ invalidBundle("Nonzero tar body padding");
12574
+ offset += count;
12575
+ this.padding -= count;
12576
+ } else {
12577
+ const count = Math.min(BLOCK - this.headerOffset, chunk.byteLength - offset);
12578
+ this.header.set(chunk.subarray(offset, offset + count), this.headerOffset);
12579
+ offset += count;
12580
+ this.headerOffset += count;
12581
+ if (this.headerOffset === BLOCK) {
12582
+ this.readHeader();
12583
+ this.headerOffset = 0;
12584
+ }
12585
+ }
12586
+ }
12587
+ }
12588
+ finish() {
12589
+ this.check();
12590
+ if (this.pending || this.padding || this.headerOffset || this.zeroBlocks < 2)
12591
+ invalidBundle("Truncated tar bundle");
12592
+ return this.entries;
12593
+ }
12594
+ readHeader() {
12595
+ this.check();
12596
+ const h = this.header;
12597
+ if (h.every((byte) => byte === 0)) {
12598
+ this.zeroBlocks++;
12599
+ return;
12600
+ }
12601
+ if (this.zeroBlocks)
12602
+ invalidBundle("Nonzero tar data after terminator");
12603
+ if (this.entries.length >= this.limits.entries)
12604
+ throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle entry limit exceeded");
12605
+ let checksum = 0;
12606
+ for (let i = 0;i < BLOCK; i++)
12607
+ checksum += i >= 148 && i < 156 ? 32 : h[i];
12608
+ if (tarOctal(h.subarray(148, 156)) !== checksum)
12609
+ invalidBundle("Invalid tar header checksum");
12610
+ if (new TextDecoder().decode(h.subarray(257, 265)) !== "ustar\x00" + "00")
12611
+ invalidBundle("Unsupported tar format");
12612
+ if (h[156] !== 48 && h[156] !== 0 || h.subarray(157, 257).some((b) => b !== 0) || h.subarray(345).some((b) => b !== 0))
12613
+ invalidBundle("Unsupported tar entry or path prefix");
12614
+ const mode = tarOctal(h.subarray(100, 108));
12615
+ if (mode > 511)
12616
+ invalidBundle("Unsupported tar permission bits");
12617
+ tarOctal(h.subarray(108, 116));
12618
+ tarOctal(h.subarray(116, 124));
12619
+ tarOctal(h.subarray(136, 148));
12620
+ const size = tarOctal(h.subarray(124, 136));
12621
+ if (size > this.limits.fileBytes || this.fileBytes + size > this.limits.decompressedBytes) {
12622
+ throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle file byte limit exceeded");
12623
+ }
12624
+ const name = h.subarray(0, 100);
12625
+ const end = name.indexOf(0);
12626
+ if (end !== -1 && name.subarray(end).some((b) => b !== 0))
12627
+ invalidBundle("Invalid tar path padding");
12628
+ const raw = end === -1 ? name : name.subarray(0, end);
12629
+ if (raw.byteLength > this.limits.pathBytes)
12630
+ throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle path byte limit exceeded");
12631
+ let path;
12632
+ try {
12633
+ path = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw);
12634
+ } catch {
12635
+ return invalidBundle("Invalid UTF-8 bundle path");
12636
+ }
12637
+ this.paths.add(path, this.limits.pathBytes, invalidBundle, () => {
12638
+ throw new SkillBundleInspectionError("BUNDLE_LIMIT", "Bundle path byte limit exceeded");
12639
+ });
12640
+ this.fileBytes += size;
12641
+ this.pending = { path, mode, bytes: new Uint8Array(new ArrayBuffer(size)) };
12642
+ this.bodyOffset = 0;
12643
+ this.padding = (BLOCK - size % BLOCK) % BLOCK;
12644
+ if (!size) {
12645
+ this.entries.push(this.pending);
12646
+ this.pending = undefined;
12647
+ }
12648
+ }
12649
+ }
12650
+ function tarOctal(field) {
12651
+ const text2 = new TextDecoder().decode(field);
12652
+ if (!/^[0-7]+[\0 ]*$/.test(text2))
12653
+ invalidBundle("Invalid tar octal field");
12654
+ const value = Number.parseInt(text2, 8);
12655
+ if (!Number.isSafeInteger(value))
12656
+ invalidBundle("Tar integer is out of range");
12657
+ return value;
12658
+ }
11977
12659
 
11978
12660
  // src/lib/skill-version.ts
11979
12661
  var SKILL_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
@@ -12352,16 +13034,16 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
12352
13034
  }
12353
13035
  return { path: target, created };
12354
13036
  }
12355
- function writePullMarker(dir, record3) {
13037
+ function writePullMarker(dir, record4) {
12356
13038
  const marker = {
12357
13039
  managedBy: "@hasna/skills",
12358
- skill: record3.skill,
12359
- source: record3.source ?? "pull",
12360
- ...record3.version ? { version: record3.version } : {},
12361
- ...record3.contentHash ? { contentHash: record3.contentHash } : {},
12362
- ...record3.sourceCommit ? { sourceCommit: record3.sourceCommit } : {},
12363
- ...record3.signature ? { signature: record3.signature } : {},
12364
- ...record3.revisionId ? { revisionId: record3.revisionId } : {},
13040
+ skill: record4.skill,
13041
+ source: record4.source ?? "pull",
13042
+ ...record4.version ? { version: record4.version } : {},
13043
+ ...record4.contentHash ? { contentHash: record4.contentHash } : {},
13044
+ ...record4.sourceCommit ? { sourceCommit: record4.sourceCommit } : {},
13045
+ ...record4.signature ? { signature: record4.signature } : {},
13046
+ ...record4.revisionId ? { revisionId: record4.revisionId } : {},
12365
13047
  syncedAt: new Date().toISOString()
12366
13048
  };
12367
13049
  writeFileSync8(join17(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
@@ -12376,19 +13058,19 @@ async function safeMeta(client, slug) {
12376
13058
  }
12377
13059
  if (!raw || typeof raw !== "object")
12378
13060
  return null;
12379
- const record3 = raw;
12380
- const kind = record3.kind === "instruction" || record3.kind === "executable" ? record3.kind : undefined;
12381
- const tags = Array.isArray(record3.tags) ? record3.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : undefined;
13061
+ const record4 = raw;
13062
+ const kind = record4.kind === "instruction" || record4.kind === "executable" ? record4.kind : undefined;
13063
+ const tags = Array.isArray(record4.tags) ? record4.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : undefined;
12382
13064
  return {
12383
- ...str(record3.displayName) ? { displayName: str(record3.displayName) } : {},
12384
- ...str(record3.description) ? { description: str(record3.description) } : {},
12385
- ...str(record3.category) ? { category: str(record3.category) } : {},
13065
+ ...str(record4.displayName) ? { displayName: str(record4.displayName) } : {},
13066
+ ...str(record4.description) ? { description: str(record4.description) } : {},
13067
+ ...str(record4.category) ? { category: str(record4.category) } : {},
12386
13068
  ...tags && tags.length ? { tags } : {},
12387
- ...str(record3.version) ? { version: str(record3.version) } : {},
13069
+ ...str(record4.version) ? { version: str(record4.version) } : {},
12388
13070
  ...kind ? { kind } : {},
12389
- ...REVISION_ID_PATTERN.test(str(record3.revisionId) ?? "") ? { revisionId: str(record3.revisionId) } : {},
12390
- ...typeof record3.skillMd === "string" && record3.skillMd.length > 0 ? { skillMd: record3.skillMd } : {},
12391
- ...str(record3.publishedSource) ? { publishedSource: str(record3.publishedSource) } : {}
13071
+ ...REVISION_ID_PATTERN.test(str(record4.revisionId) ?? "") ? { revisionId: str(record4.revisionId) } : {},
13072
+ ...typeof record4.skillMd === "string" && record4.skillMd.length > 0 ? { skillMd: record4.skillMd } : {},
13073
+ ...str(record4.publishedSource) ? { publishedSource: str(record4.publishedSource) } : {}
12392
13074
  };
12393
13075
  }
12394
13076
  function pickCorpusOptions(options) {
@@ -12397,8 +13079,8 @@ function pickCorpusOptions(options) {
12397
13079
  function extractSlug(entry) {
12398
13080
  if (!entry || typeof entry !== "object")
12399
13081
  return;
12400
- const record3 = entry;
12401
- return str(record3.slug) ?? str(record3.name);
13082
+ const record4 = entry;
13083
+ return str(record4.slug) ?? str(record4.name);
12402
13084
  }
12403
13085
  function dedupe(values) {
12404
13086
  return [...new Set(values)];
@@ -12524,7 +13206,7 @@ import { dirname as dirname7, relative as relative4 } from "path";
12524
13206
  // package.json
12525
13207
  var package_default = {
12526
13208
  name: "@hasna/skills",
12527
- version: "0.5.2",
13209
+ version: "0.5.3",
12528
13210
  description: "Skills library for AI coding agents",
12529
13211
  type: "module",
12530
13212
  bin: {
@@ -14035,7 +14717,7 @@ class SkillsPostgresSyncStore {
14035
14717
  }
14036
14718
  async upsertRecords(records) {
14037
14719
  let count = 0;
14038
- for (const record3 of records) {
14720
+ for (const record4 of records) {
14039
14721
  await this.client.query([
14040
14722
  "INSERT INTO skills_sync_records",
14041
14723
  "(scope, kind, id, updated_at, deleted_at, source, payload)",
@@ -14046,13 +14728,13 @@ class SkillsPostgresSyncStore {
14046
14728
  "source = EXCLUDED.source,",
14047
14729
  "payload = EXCLUDED.payload"
14048
14730
  ].join(" "), [
14049
- record3.scope,
14050
- record3.kind,
14051
- record3.id,
14052
- record3.updatedAt,
14053
- record3.deletedAt ?? null,
14054
- record3.source ?? null,
14055
- JSON.stringify(record3.payload)
14731
+ record4.scope,
14732
+ record4.kind,
14733
+ record4.id,
14734
+ record4.updatedAt,
14735
+ record4.deletedAt ?? null,
14736
+ record4.source ?? null,
14737
+ JSON.stringify(record4.payload)
14056
14738
  ]);
14057
14739
  count += 1;
14058
14740
  }
@@ -14994,6 +15676,101 @@ function writeStationHydration(options) {
14994
15676
  manifestPath: hydrationManifestPath
14995
15677
  };
14996
15678
  }
15679
+ // src/lib/remote-invitation-recovery.ts
15680
+ class InvitationEmailInputError extends Error {
15681
+ code = "INVITATION_EMAIL_INPUT_INVALID";
15682
+ constructor() {
15683
+ super("Use an explicit Skills API URL, exact invitation and retained challenge IDs, secret input, and deliberate confirmation.");
15684
+ this.name = "InvitationEmailInputError";
15685
+ }
15686
+ }
15687
+ var refusals = {
15688
+ INVALID_REQUEST: [400, "Invitation recovery parameters were refused."],
15689
+ ORIGIN_REQUIRED: [403, "Invitation recovery requires the configured site origin."],
15690
+ INVITATION_PROOF_UNAVAILABLE: [401, "Invitation proof is unavailable. Sign in or explicitly request another recovery code."],
15691
+ RATE_LIMITED: [429, "Invitation verification is rate limited. Wait before another deliberate action."],
15692
+ INVITATION_BUSY: [503, "Invitation is busy. Sign in to check membership before another deliberate action."],
15693
+ INVITATION_DELIVERY_UNAVAILABLE: [503, "Invitation recovery is unavailable on this service."]
15694
+ };
15695
+
15696
+ class RemoteInvitationEmailError extends Error {
15697
+ code;
15698
+ status;
15699
+ constructor(code) {
15700
+ super(refusals[code][1]);
15701
+ this.code = code;
15702
+ this.name = "RemoteInvitationEmailError";
15703
+ this.status = refusals[code][0];
15704
+ }
15705
+ }
15706
+
15707
+ class RemoteInvitationEmailUnconfirmedError extends Error {
15708
+ action;
15709
+ code = "INVITATION_EMAIL_UNCONFIRMED";
15710
+ constructor(action) {
15711
+ super(action === "challenge" ? "The recovery challenge outcome is unconfirmed. Retain the same invitation, challenge ID and server. Check your inbox; never rotate the challenge or retry automatically. No delivery is confirmed." : "Invitation acceptance is unconfirmed. Use fresh ordinary sign-in to inspect available memberships. Do not retry acceptance automatically; explicitly request another recovery code only if needed. Saved credentials are unchanged.");
15712
+ this.action = action;
15713
+ this.name = "RemoteInvitationEmailUnconfirmedError";
15714
+ }
15715
+ }
15716
+ var record4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
15717
+ var uuid4 = (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);
15718
+ function invitationEmailIds(invitationId, challengeId) {
15719
+ if (!uuid4(invitationId) || !uuid4(challengeId))
15720
+ throw new InvitationEmailInputError;
15721
+ return { invitationId, challengeId };
15722
+ }
15723
+ function invitationEmailInput(action, input) {
15724
+ const keys = ["invitationId", "token", "challengeId", "confirm", ...action === "accept" ? ["code"] : []];
15725
+ if (!record4(input) || Object.keys(input).length !== keys.length || keys.some((key) => !Object.hasOwn(input, key)))
15726
+ throw new InvitationEmailInputError;
15727
+ const value = { ...input };
15728
+ const ids = invitationEmailIds(value.invitationId, value.challengeId);
15729
+ if (value.confirm !== true || typeof value.token !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(value.token) || action === "accept" && (typeof value.code !== "string" || !/^\d{6}$/.test(value.code)))
15730
+ throw new InvitationEmailInputError;
15731
+ return { ...ids, token: value.token, confirm: true, ...action === "accept" ? { code: value.code } : {} };
15732
+ }
15733
+ async function requestInvitationEmail(origin, action, input) {
15734
+ const value = invitationEmailInput(action, input);
15735
+ let target;
15736
+ try {
15737
+ target = normalizeSkillsApiOrigin(origin);
15738
+ } catch {
15739
+ throw new InvitationEmailInputError;
15740
+ }
15741
+ const body = JSON.stringify({ invitationId: value.invitationId, token: value.token, challengeId: value.challengeId, ...action === "accept" ? { code: value.code } : {} });
15742
+ try {
15743
+ const response = await fetch(`${target}/api/v1/account/invitations/email-${action}`, {
15744
+ method: "POST",
15745
+ headers: { "Content-Type": "application/json" },
15746
+ body,
15747
+ credentials: "omit",
15748
+ redirect: "error",
15749
+ cache: "no-store",
15750
+ referrerPolicy: "no-referrer",
15751
+ signal: AbortSignal.timeout(15000)
15752
+ });
15753
+ const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(await readBoundedResponse(response, 4096)));
15754
+ if (!response.ok && record4(parsed) && typeof parsed.code === "string" && Object.hasOwn(refusals, parsed.code)) {
15755
+ const code = parsed.code;
15756
+ if (response.status === refusals[code][0])
15757
+ throw new RemoteInvitationEmailError(code);
15758
+ }
15759
+ if (!record4(parsed))
15760
+ throw new RemoteInvitationEmailUnconfirmedError(action);
15761
+ if (action === "challenge" && response.status === 202 && parsed.challengeId === value.challengeId && parsed.expiresIn === 600 && typeof parsed.message === "string" && parsed.message.length <= 256) {
15762
+ return { challengeId: value.challengeId, message: "If this invitation is eligible, a verification code will arrive. Delivery is not confirmed.", expiresIn: 600 };
15763
+ }
15764
+ if (action === "accept" && response.status === 200 && uuid4(parsed.organizationId) && uuid4(parsed.membershipId) && parsed.accepted === true && parsed.changed === true && parsed.signInRequired === true) {
15765
+ return { organizationId: parsed.organizationId, membershipId: parsed.membershipId, accepted: true, changed: true, signInRequired: true };
15766
+ }
15767
+ } catch (error) {
15768
+ if (error instanceof RemoteInvitationEmailError)
15769
+ throw error;
15770
+ }
15771
+ throw new RemoteInvitationEmailUnconfirmedError(action);
15772
+ }
15773
+
14997
15774
  // src/lib/remote-auth.ts
14998
15775
  var MAX_ERROR_DETAIL_LENGTH = 200;
14999
15776
 
@@ -15034,10 +15811,10 @@ async function requestAuthApi(instance, path, options) {
15034
15811
  const text2 = await res.text();
15035
15812
  const body = text2 ? parseJsonBody(text2) : {};
15036
15813
  if (!res.ok) {
15037
- const record3 = isRecord5(body) ? body : {};
15038
- const detail = typeof record3.detail === "string" ? record3.detail : undefined;
15039
- const error = typeof record3.error === "string" ? record3.error : undefined;
15040
- const code = typeof record3.code === "string" ? record3.code : undefined;
15814
+ const record5 = isRecord5(body) ? body : {};
15815
+ const detail = typeof record5.detail === "string" ? record5.detail : undefined;
15816
+ const error = typeof record5.error === "string" ? record5.error : undefined;
15817
+ const code = typeof record5.code === "string" ? record5.code : undefined;
15041
15818
  throw new HostedApiError(detail || error || `${res.status} ${res.statusText}`, {
15042
15819
  status: res.status,
15043
15820
  code,
@@ -15071,11 +15848,17 @@ class RemoteSkillsAuthClient {
15071
15848
  constructor(apiUrl) {
15072
15849
  this.apiOrigin = normalizeSkillsApiOrigin(apiUrl);
15073
15850
  }
15074
- requestCode(email) {
15075
- return this.request("/api/auth/login", { method: "POST", body: JSON.stringify({ email }) });
15851
+ requestInvitationEmailChallenge(input) {
15852
+ return requestInvitationEmail(this.apiOrigin, "challenge", input);
15853
+ }
15854
+ acceptInvitationEmailChallenge(input) {
15855
+ return requestInvitationEmail(this.apiOrigin, "accept", input);
15856
+ }
15857
+ requestCode(email2) {
15858
+ return this.request("/api/auth/login", { method: "POST", body: JSON.stringify({ email: email2 }) });
15076
15859
  }
15077
- verifyCode(email, code) {
15078
- return this.request("/api/auth/verify", { method: "POST", body: JSON.stringify({ email, code }) });
15860
+ verifyCode(email2, code) {
15861
+ return this.request("/api/auth/verify", { method: "POST", body: JSON.stringify({ email: email2, code }) });
15079
15862
  }
15080
15863
  startDevice() {
15081
15864
  return this.request("/api/auth/device/start", { method: "POST", body: JSON.stringify({ client: "skills-sdk" }) });
@@ -15083,34 +15866,34 @@ class RemoteSkillsAuthClient {
15083
15866
  pollDevice(deviceCode) {
15084
15867
  return this.request("/api/auth/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) });
15085
15868
  }
15086
- async sessionClient(email, code, context) {
15869
+ async sessionClient(email2, code, context) {
15087
15870
  if (context !== undefined) {
15088
15871
  const target = workspaceContext(context), apiOrigin2 = this.apiOrigin;
15089
- const session = await this.switchWorkspace(email, code, target);
15872
+ const session = await this.switchWorkspace(email2, code, target);
15090
15873
  return new RemoteSkillsClient(session.token, apiOrigin2);
15091
15874
  }
15092
15875
  const apiOrigin = this.apiOrigin;
15093
- if (!email.includes("@") || !/^\d{6}$/.test(code))
15876
+ if (!email2.includes("@") || !/^\d{6}$/.test(code))
15094
15877
  throw new Error("Fresh email and six-digit verification code are required to manage this account");
15095
- const login = await this.verifyCode(email, code);
15878
+ const login = await this.verifyCode(email2, code);
15096
15879
  if (!login || typeof login.token !== "string" || !login.token)
15097
15880
  throw new Error("The server did not return an authorized account session");
15098
15881
  return new RemoteSkillsClient(login.token, apiOrigin);
15099
15882
  }
15100
- async listAccountWorkspaces(email, code, expectedUserId) {
15101
- const login = await this.workspaceLogin(email, code, expectedUserId);
15883
+ async listAccountWorkspaces(email2, code, expectedUserId) {
15884
+ const login = await this.workspaceLogin(email2, code, expectedUserId);
15102
15885
  const result = await new RemoteSkillsClient(login.token, login.apiOrigin).listAccountWorkspaces(login.userId);
15103
15886
  return { userId: login.userId, ...result };
15104
15887
  }
15105
- async switchWorkspace(email, code, context) {
15888
+ async switchWorkspace(email2, code, context) {
15106
15889
  const target = workspaceContext(context);
15107
- const login = await this.workspaceLogin(email, code, target.userId);
15890
+ const login = await this.workspaceLogin(email2, code, target.userId);
15108
15891
  return new RemoteSkillsClient(login.token, login.apiOrigin).switchWorkspace(target);
15109
15892
  }
15110
- async workspaceLogin(email, code, expectedUserId) {
15893
+ async workspaceLogin(email2, code, expectedUserId) {
15111
15894
  const expected = expectedUserId === undefined ? undefined : workspaceExpectedUserId(expectedUserId);
15112
15895
  const apiOrigin = this.apiOrigin;
15113
- if (typeof email !== "string" || !email.includes("@") || typeof code !== "string" || !/^\d{6}$/.test(code))
15896
+ if (typeof email2 !== "string" || !email2.includes("@") || typeof code !== "string" || !/^\d{6}$/.test(code))
15114
15897
  throw new Error("Fresh email and six-digit verification code are required to manage this account");
15115
15898
  let response;
15116
15899
  try {
@@ -15120,7 +15903,7 @@ class RemoteSkillsAuthClient {
15120
15903
  credentials: "omit",
15121
15904
  signal: AbortSignal.timeout(15000),
15122
15905
  headers: { "Content-Type": "application/json" },
15123
- body: JSON.stringify({ email, code })
15906
+ body: JSON.stringify({ email: email2, code })
15124
15907
  });
15125
15908
  } catch {
15126
15909
  throw new HostedApiError("Unable to verify the Skills account.");
@@ -15137,40 +15920,67 @@ class RemoteSkillsAuthClient {
15137
15920
  }
15138
15921
  return { ...parseWorkspaceLogin(value, expected), apiOrigin };
15139
15922
  }
15140
- async createApiKey(email, code, name, scopes, context) {
15923
+ async listWorkspaceInvitations(email2, code, context, options = {}) {
15924
+ const target = workspaceContext(context), captured = invitationInput("list", options);
15925
+ return (await this.sessionClient(email2, code, target)).listWorkspaceInvitations(target, captured);
15926
+ }
15927
+ async getWorkspaceInvitation(email2, code, context, invitationId) {
15928
+ const target = workspaceContext(context), captured = invitationInput("get", { invitationId });
15929
+ return (await this.sessionClient(email2, code, target)).getWorkspaceInvitation(target, captured.invitationId);
15930
+ }
15931
+ async issueWorkspaceInvitation(email2, code, context, input) {
15932
+ const target = workspaceContext(context), captured = invitationInput("issue", input);
15933
+ return (await this.sessionClient(email2, code, target)).issueWorkspaceInvitation(target, captured);
15934
+ }
15935
+ async resendWorkspaceInvitation(email2, code, context, invitationId, input) {
15936
+ const target = workspaceContext(context), captured = invitationInput("resend", { ...input, invitationId });
15937
+ const { invitationId: id, ...options } = captured;
15938
+ return (await this.sessionClient(email2, code, target)).resendWorkspaceInvitation(target, id, options);
15939
+ }
15940
+ async revokeWorkspaceInvitation(email2, code, context, invitationId, input) {
15941
+ const target = workspaceContext(context), captured = invitationInput("revoke", { ...input, invitationId });
15942
+ const { invitationId: id, ...options } = captured;
15943
+ return (await this.sessionClient(email2, code, target)).revokeWorkspaceInvitation(target, id, options);
15944
+ }
15945
+ async acceptWorkspaceInvitation(email2, code, context, invitationId, input) {
15946
+ const target = workspaceContext(context), captured = invitationInput("accept", { ...input, invitationId });
15947
+ const { invitationId: id, ...options } = captured;
15948
+ return (await this.sessionClient(email2, code, target)).acceptWorkspaceInvitation(target, id, options);
15949
+ }
15950
+ async createApiKey(email2, code, name, scopes, context) {
15141
15951
  const capturedScopes = scopes === undefined ? undefined : [...scopes];
15142
- return (await this.sessionClient(email, code, context)).createApiKey(name, capturedScopes);
15952
+ return (await this.sessionClient(email2, code, context)).createApiKey(name, capturedScopes);
15143
15953
  }
15144
- async listApiKeys(email, code, context) {
15145
- return (await this.sessionClient(email, code, context)).listApiKeys();
15954
+ async listApiKeys(email2, code, context) {
15955
+ return (await this.sessionClient(email2, code, context)).listApiKeys();
15146
15956
  }
15147
- async revokeApiKey(email, code, keyId, context) {
15148
- return (await this.sessionClient(email, code, context)).revokeApiKey(keyId);
15957
+ async revokeApiKey(email2, code, keyId, context) {
15958
+ return (await this.sessionClient(email2, code, context)).revokeApiKey(keyId);
15149
15959
  }
15150
- async updateProfile(email, code, input, context) {
15960
+ async updateProfile(email2, code, input, context) {
15151
15961
  const body = customerNamePatch(input, "displayName");
15152
- return (await this.sessionClient(email, code, context)).updateProfile({ displayName: body.displayName });
15962
+ return (await this.sessionClient(email2, code, context)).updateProfile({ displayName: body.displayName });
15153
15963
  }
15154
- async updateCurrentWorkspace(email, code, input, context) {
15964
+ async updateCurrentWorkspace(email2, code, input, context) {
15155
15965
  const body = customerNamePatch(input, "name");
15156
- return (await this.sessionClient(email, code, context)).updateCurrentWorkspace({ name: body.name });
15966
+ return (await this.sessionClient(email2, code, context)).updateCurrentWorkspace({ name: body.name });
15157
15967
  }
15158
- async listWorkspaceMembers(email, code, options = {}, context) {
15968
+ async listWorkspaceMembers(email2, code, options = {}, context) {
15159
15969
  workspaceMembersQuery(options);
15160
15970
  const captured = { ...options };
15161
- return (await this.sessionClient(email, code, context)).listWorkspaceMembers(captured);
15971
+ return (await this.sessionClient(email2, code, context)).listWorkspaceMembers(captured);
15162
15972
  }
15163
- async setWorkspaceMemberRole(email, code, membershipId, input, context) {
15973
+ async setWorkspaceMemberRole(email2, code, membershipId, input, context) {
15164
15974
  const captured = workspaceMemberRoleInput(membershipId, input);
15165
- return (await this.sessionClient(email, code, context)).setWorkspaceMemberRole(captured.membershipId, captured.body);
15975
+ return (await this.sessionClient(email2, code, context)).setWorkspaceMemberRole(captured.membershipId, captured.body);
15166
15976
  }
15167
- async leaveWorkspace(email, code, context, input) {
15977
+ async leaveWorkspace(email2, code, context, input) {
15168
15978
  const captured = workspaceLeaveInput(context, input);
15169
- return (await this.sessionClient(email, code, captured.context)).leaveWorkspace(captured.context, captured.input);
15979
+ return (await this.sessionClient(email2, code, captured.context)).leaveWorkspace(captured.context, captured.input);
15170
15980
  }
15171
- async removeWorkspaceMember(email, code, membershipId, input, context) {
15981
+ async removeWorkspaceMember(email2, code, membershipId, input, context) {
15172
15982
  const captured = workspaceMemberRemovalInput(membershipId, input);
15173
- return (await this.sessionClient(email, code, context)).removeWorkspaceMember(captured.membershipId, captured.body);
15983
+ return (await this.sessionClient(email2, code, context)).removeWorkspaceMember(captured.membershipId, captured.body);
15174
15984
  }
15175
15985
  request(path, options) {
15176
15986
  if (!["/api/auth/login", "/api/auth/verify", "/api/auth/device/start", "/api/auth/device/token", "/api/auth/keys", "/api/auth/whoami"].includes(path))
@@ -15187,6 +15997,7 @@ export {
15187
15997
  writeManagedAgentSkill,
15188
15998
  writeCorpusSkill,
15189
15999
  walkEntries,
16000
+ verifyContentHashFromEntries,
15190
16001
  verifyContentHash,
15191
16002
  validateToolPrimitiveCoverage,
15192
16003
  validateStationId,
@@ -15223,6 +16034,7 @@ export {
15223
16034
  sanitizePublicDiscoveryText,
15224
16035
  runSkill,
15225
16036
  runPortableSkill,
16037
+ revisionIdOf,
15226
16038
  resolveSyncAgents,
15227
16039
  resolveStorageConfig,
15228
16040
  resolveSkillsNativeStorageConfig,
@@ -15350,6 +16162,7 @@ export {
15350
16162
  createMcpContractManifest,
15351
16163
  createLocalSkillManifest,
15352
16164
  configuredSkillsApiUrl,
16165
+ computeContentHashFromEntries,
15353
16166
  computeContentHash,
15354
16167
  completeSkillRun,
15355
16168
  clearRegistryCache,
@@ -15361,6 +16174,7 @@ export {
15361
16174
  addSchedule,
15362
16175
  adaptSkillMdForAgent,
15363
16176
  WorkspaceLeaveInputError,
16177
+ WorkspaceInvitationInputError,
15364
16178
  WorkspaceIdentityMismatchError,
15365
16179
  WorkspaceContextInputError,
15366
16180
  TOOL_PRIMITIVE_SCHEMA_VERSION,
@@ -15398,10 +16212,15 @@ export {
15398
16212
  RemoteWorkspaceMemberError,
15399
16213
  RemoteWorkspaceLeaveUnconfirmedError,
15400
16214
  RemoteWorkspaceLeaveError,
16215
+ RemoteWorkspaceInvitationUnconfirmedError,
16216
+ RemoteWorkspaceInvitationReadError,
16217
+ RemoteWorkspaceInvitationError,
15401
16218
  RemoteSkillsClient,
15402
16219
  RemoteSkillsAuthClient,
15403
16220
  RemoteRouteUnsupportedError,
15404
16221
  RemoteRequestError,
16222
+ RemoteInvitationEmailUnconfirmedError,
16223
+ RemoteInvitationEmailError,
15405
16224
  RemoteCreditApprovalError,
15406
16225
  RemoteCapabilityUnavailableError,
15407
16226
  REMOTE_SKILL_RUN_CONTRACT_VERSION,
@@ -15413,8 +16232,11 @@ export {
15413
16232
  PORTABLE_SKILL_DEFAULT_VERSION,
15414
16233
  MissingSkillsFleetError,
15415
16234
  MCP_CONTRACT_SCHEMA_VERSION,
16235
+ InvitationEmailInputError,
15416
16236
  HostedApiError,
15417
16237
  DEFAULT_EXPORT_DIR,
16238
+ ContentHashInputError,
16239
+ CONTENT_HASH_LIMITS,
15418
16240
  CATEGORIES,
15419
16241
  BASIC_SKILL_NAMES,
15420
16242
  ARTICLE_GENERATION_SLUG,