// src/connectors/types.ts
var ResourceContention = class extends Error {
  constructor(message, alternatives = [], currentState) {
    super(message);
    this.alternatives = alternatives;
    this.currentState = currentState;
  }
  alternatives;
  currentState;
  name = "ResourceContention";
};
var CredentialsExpired = class extends Error {
  constructor(message, dataSourceId) {
    super(message);
    this.dataSourceId = dataSourceId;
  }
  dataSourceId;
  name = "CredentialsExpired";
};
function validateConnectorManifest(manifest) {
  const issues = [];
  if (!manifest.kind.trim()) issues.push({ path: "kind", message: "kind is required" });
  if (!manifest.displayName.trim()) issues.push({ path: "displayName", message: "displayName is required" });
  const seen = /* @__PURE__ */ new Set();
  for (const [index, capability] of manifest.capabilities.entries()) {
    const path = `capabilities[${index}]`;
    if (!capability.name.trim()) issues.push({ path: `${path}.name`, message: "capability name is required" });
    if (seen.has(capability.name)) issues.push({ path: `${path}.name`, message: `duplicate capability name: ${capability.name}` });
    seen.add(capability.name);
    if (capability.class === "mutation") {
      if (!capability.cas) issues.push({ path: `${path}.cas`, message: "mutation capability must declare a CAS strategy" });
      if (manifest.defaultConsistencyModel === "authoritative" && capability.cas === "none") {
        issues.push({ path: `${path}.cas`, message: 'authoritative mutations cannot use cas="none"' });
      }
    }
  }
  if (manifest.rateLimit) {
    if (!Number.isFinite(manifest.rateLimit.requests) || manifest.rateLimit.requests <= 0) {
      issues.push({ path: "rateLimit.requests", message: "rateLimit.requests must be positive" });
    }
    if (!Number.isFinite(manifest.rateLimit.windowMs) || manifest.rateLimit.windowMs <= 0) {
      issues.push({ path: "rateLimit.windowMs", message: "rateLimit.windowMs must be positive" });
    }
  }
  return { ok: issues.length === 0, issues };
}
function assertValidConnectorManifest(manifest) {
  const result = validateConnectorManifest(manifest);
  if (!result.ok) {
    throw new Error(`Invalid connector manifest ${manifest.kind || "<unknown>"}: ${result.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
  }
}

// src/connectors/adapters/tangle-id.ts
var DEFAULT_TANGLE_PLATFORM_URL = "https://id.tangle.tools";
var PLATFORM_FETCH_TIMEOUT_MS = 5e3;
var TANGLE_API_KEY_PREFIX = "sk-tan-";
var TANGLE_SERVICE_TOKEN_PREFIX = "svc_";
var TangleIdentityUnreachableError = class extends Error {
  name = "TangleIdentityUnreachableError";
  status;
  constructor(message, opts) {
    super(message, opts);
    this.status = opts?.status;
  }
};
function tangleIdentity(opts = {}) {
  const client = createTangleIdentityClient(opts);
  const adapter = {
    manifest: {
      kind: "tangle-id",
      displayName: "Tangle Identity",
      description: "Verify Tangle session cookies and API keys issued by id.tangle.tools, resolve user + workspace identity, and gate capability discovery against the calling workspace scopes.",
      auth: { kind: "api-key", hint: "Tangle platform service token (svc_*)." },
      category: "other",
      defaultConsistencyModel: "authoritative",
      // 50 req/s per service token matches the platform's documented
      // S2S budget. We meter on our side so a chatty product doesn't
      // burn the shared service-token quota and block the rest.
      rateLimit: { requests: 50, windowMs: 1e3, scope: "oauth-client" },
      capabilities: [
        {
          name: "verify_token",
          class: "read",
          description: "Verify a session cookie or sk-tan-* API key. Returns { valid, userId, workspaceId, scopes, expiresAt } or { valid: false, reason }.",
          parameters: {
            type: "object",
            properties: {
              token: { type: "string", minLength: 1 }
            },
            required: ["token"]
          }
        },
        {
          name: "get_user",
          class: "read",
          description: "Read the user profile (id, email, name, image) for a verified userId.",
          parameters: {
            type: "object",
            properties: { userId: { type: "string", minLength: 1 } },
            required: ["userId"]
          }
        },
        {
          name: "list_workspaces",
          class: "read",
          description: "List workspaces (teams + the personal workspace) the user belongs to, with role + effective scopes.",
          parameters: {
            type: "object",
            properties: { userId: { type: "string", minLength: 1 } },
            required: ["userId"]
          }
        },
        {
          name: "switch_workspace",
          class: "mutation",
          description: "Resolve a workspace by id and return its effective scope set. Stateless on this adapter \u2014 caller persists the workspaceId in its own session store.",
          parameters: {
            type: "object",
            properties: {
              userId: { type: "string", minLength: 1 },
              workspaceId: { type: "string", minLength: 1 }
            },
            required: ["userId", "workspaceId"]
          },
          cas: "native-idempotency",
          externalEffect: false
        },
        {
          name: "revoke_session",
          class: "mutation",
          description: "Revoke a session token or API key. Idempotent.",
          parameters: {
            type: "object",
            properties: { token: { type: "string", minLength: 1 } },
            required: ["token"]
          },
          cas: "native-idempotency",
          externalEffect: true
        }
      ]
    },
    async executeRead(inv) {
      if (inv.capabilityName === "verify_token") {
        const token = readStringArg(inv.args, "token");
        const result = await client.verifyToken(token);
        return { data: result, fetchedAt: Date.now() };
      }
      if (inv.capabilityName === "get_user") {
        const userId = readStringArg(inv.args, "userId");
        const user = await client.getUser(userId);
        return { data: user, fetchedAt: Date.now() };
      }
      if (inv.capabilityName === "list_workspaces") {
        const userId = readStringArg(inv.args, "userId");
        const workspaces = await client.listWorkspaces(userId);
        return { data: { workspaces }, fetchedAt: Date.now() };
      }
      throw new Error(`tangle-id: unknown read capability ${inv.capabilityName}`);
    },
    async executeMutation(inv) {
      if (inv.capabilityName === "switch_workspace") {
        const userId = readStringArg(inv.args, "userId");
        const workspaceId = readStringArg(inv.args, "workspaceId");
        const result = await client.switchWorkspace(userId, workspaceId);
        return {
          status: "committed",
          data: result,
          committedAt: Date.now(),
          idempotentReplay: false
        };
      }
      if (inv.capabilityName === "revoke_session") {
        const token = readStringArg(inv.args, "token");
        await client.revokeSession(token);
        return {
          status: "committed",
          data: { ok: true },
          committedAt: Date.now(),
          idempotentReplay: false
        };
      }
      throw new Error(`tangle-id: unknown mutation capability ${inv.capabilityName}`);
    },
    async test(source) {
      try {
        const ok = await client.ping();
        if (!ok) return { ok: false, reason: "id.tangle.tools /health returned non-200" };
        return { ok: true };
      } catch (err) {
        if (err instanceof CredentialsExpired) {
          return { ok: false, reason: "service token rejected \u2014 rotate TANGLE_SERVICE_TOKEN" };
        }
        return { ok: false, reason: err instanceof Error ? err.message : String(err) };
      }
    }
  };
  return adapter;
}
function createTangleIdentityClient(opts = {}) {
  const baseUrl = (opts.baseUrl ?? DEFAULT_TANGLE_PLATFORM_URL).replace(/\/+$/, "");
  const serviceToken = opts.serviceToken;
  const serviceName = opts.serviceName ?? "integrations";
  const fetchImpl = opts.fetchImpl ?? fetch;
  const timeoutMs = opts.timeoutMs ?? PLATFORM_FETCH_TIMEOUT_MS;
  function s2sHeaders() {
    if (!serviceToken) {
      throw new TangleIdentityUnreachableError(
        "tangle-id: serviceToken is required for service-to-service calls (verify, get_user, list_workspaces, revoke)"
      );
    }
    return {
      "content-type": "application/json",
      authorization: `Bearer ${serviceToken}`,
      "x-service-name": serviceName
    };
  }
  async function jsonFetch(path, init) {
    let res;
    try {
      res = await fetchImpl(`${baseUrl}${path}`, {
        ...init,
        signal: init.signal ?? AbortSignal.timeout(timeoutMs)
      });
    } catch (err) {
      throw new TangleIdentityUnreachableError(
        `tangle-id: request to ${path} failed`,
        { cause: err }
      );
    }
    return res;
  }
  async function readErrorDetail(res) {
    try {
      const text = await res.text();
      return text.trim().slice(0, 200);
    } catch {
      return "";
    }
  }
  async function verifyApiKey(token) {
    const res = await jsonFetch("/v1/keys/verify", {
      method: "POST",
      headers: s2sHeaders(),
      body: JSON.stringify({ key: token })
    });
    if (res.status === 401) {
      return { valid: false, reason: "service_token_refused" };
    }
    if (!res.ok) {
      throw new TangleIdentityUnreachableError(
        `tangle-id: /v1/keys/verify returned ${res.status}: ${await readErrorDetail(res)}`,
        { status: res.status }
      );
    }
    const body = await res.json().catch(() => null);
    if (!body || typeof body.valid !== "boolean") {
      return { valid: false, reason: "malformed" };
    }
    if (!body.valid || !body.userId) {
      return { valid: false, reason: "revoked" };
    }
    const scopes = Array.isArray(body.allowedModels) ? body.allowedModels.filter((value) => typeof value === "string") : [];
    if (body.product) scopes.push(`product:${body.product}`);
    const expiresAt = typeof body.expiresAt === "string" && body.expiresAt ? Date.parse(body.expiresAt) : void 0;
    return {
      valid: true,
      kind: "api_key",
      userId: body.userId,
      workspaceId: body.ownerType === "team" && body.ownerId ? body.ownerId : body.userId,
      ownerType: body.ownerType ?? "user",
      scopes,
      ...Number.isFinite(expiresAt) ? { expiresAt } : {},
      ...body.keyId ? { credentialId: body.keyId } : {},
      ...body.product ? { product: body.product } : {}
    };
  }
  async function verifySession(token) {
    const res = await jsonFetch("/api/auth/get-session", {
      method: "GET",
      headers: {
        accept: "application/json",
        authorization: `Bearer ${token}`
      }
    });
    if (res.status === 401 || res.status === 403) {
      return { valid: false, reason: "expired" };
    }
    if (!res.ok) {
      throw new TangleIdentityUnreachableError(
        `tangle-id: /api/auth/get-session returned ${res.status}: ${await readErrorDetail(res)}`,
        { status: res.status }
      );
    }
    const body = await res.json().catch(() => null);
    if (!body || !body.user || typeof body.user.id !== "string") {
      return { valid: false, reason: "expired" };
    }
    const expiresAtRaw = body.session?.expiresAt;
    const expiresAt = expiresAtRaw ? Date.parse(expiresAtRaw) : NaN;
    return {
      valid: true,
      kind: "session",
      userId: body.user.id,
      workspaceId: body.session?.activeTeamId || body.user.id,
      ownerType: body.session?.activeTeamId ? "team" : "user",
      scopes: [],
      ...Number.isFinite(expiresAt) ? { expiresAt } : {},
      ...body.session?.id ? { credentialId: body.session.id } : {}
    };
  }
  return {
    async verifyToken(token) {
      if (!token || typeof token !== "string") {
        return { valid: false, reason: "malformed" };
      }
      if (token.startsWith(TANGLE_SERVICE_TOKEN_PREFIX)) {
        return { valid: false, reason: "service_token_refused" };
      }
      if (token.startsWith(TANGLE_API_KEY_PREFIX)) {
        return verifyApiKey(token);
      }
      return verifySession(token);
    },
    async getUser(userId) {
      if (!userId) {
        throw new TangleIdentityUnreachableError("tangle-id: getUser requires a non-empty userId");
      }
      const res = await jsonFetch(`/v1/users/${encodeURIComponent(userId)}`, {
        method: "GET",
        headers: s2sHeaders()
      });
      if (res.status === 404) {
        throw new TangleIdentityUnreachableError(`tangle-id: user ${userId} not found`, { status: 404 });
      }
      if (!res.ok) {
        throw new TangleIdentityUnreachableError(
          `tangle-id: /v1/users/${userId} returned ${res.status}: ${await readErrorDetail(res)}`,
          { status: res.status }
        );
      }
      const body = await res.json().catch(() => null);
      const data = body?.data;
      if (!data || typeof data.id !== "string") {
        throw new TangleIdentityUnreachableError("tangle-id: /v1/users response had an invalid shape");
      }
      return {
        id: data.id,
        ...typeof data.email === "string" ? { email: data.email } : {},
        ...data.name !== void 0 ? { name: data.name } : {},
        ...data.image !== void 0 ? { image: data.image } : {}
      };
    },
    async listWorkspaces(userId) {
      const res = await jsonFetch("/v1/teams", {
        method: "GET",
        headers: { ...s2sHeaders(), "x-platform-user-id": userId }
      });
      if (!res.ok) {
        throw new TangleIdentityUnreachableError(
          `tangle-id: /v1/teams returned ${res.status}: ${await readErrorDetail(res)}`,
          { status: res.status }
        );
      }
      const body = await res.json().catch(() => null);
      const rows = Array.isArray(body?.data) ? body.data : [];
      const workspaces = [];
      for (const row of rows) {
        if (!row || typeof row.id !== "string" || typeof row.name !== "string") continue;
        const role = row.role === "owner" || row.role === "admin" ? row.role : "member";
        const scopes = Array.isArray(row.scopes) ? row.scopes.filter((value) => typeof value === "string") : [];
        workspaces.push({
          id: row.id,
          name: row.name,
          role,
          isPersonal: Boolean(row.isPersonal) || row.id === userId,
          scopes
        });
      }
      return workspaces;
    },
    async switchWorkspace(userId, workspaceId) {
      const workspaces = await this.listWorkspaces(userId);
      const match = workspaces.find((w) => w.id === workspaceId);
      if (!match) {
        throw new TangleIdentityUnreachableError(
          `tangle-id: workspace ${workspaceId} not found for user ${userId}`,
          { status: 404 }
        );
      }
      return { ok: true, workspaceId: match.id, scopes: match.scopes };
    },
    async revokeSession(token) {
      if (!token) return;
      if (token.startsWith(TANGLE_SERVICE_TOKEN_PREFIX)) {
        throw new TangleIdentityUnreachableError(
          "tangle-id: refusing to revoke a service token \u2014 rotate it instead"
        );
      }
      if (token.startsWith(TANGLE_API_KEY_PREFIX)) {
        const v = await verifyApiKey(token);
        if (!v.valid || !v.credentialId) return;
        const res2 = await jsonFetch(`/v1/keys/${encodeURIComponent(v.credentialId)}`, {
          method: "DELETE",
          headers: s2sHeaders()
        });
        if (res2.status === 404) return;
        if (!res2.ok) {
          throw new TangleIdentityUnreachableError(
            `tangle-id: DELETE /v1/keys/${v.credentialId} returned ${res2.status}: ${await readErrorDetail(res2)}`,
            { status: res2.status }
          );
        }
        return;
      }
      const res = await jsonFetch("/api/auth/sign-out", {
        method: "POST",
        headers: {
          "content-type": "application/json",
          authorization: `Bearer ${token}`
        },
        body: "{}"
      });
      if (res.status >= 500) {
        throw new TangleIdentityUnreachableError(
          `tangle-id: /api/auth/sign-out returned ${res.status}: ${await readErrorDetail(res)}`,
          { status: res.status }
        );
      }
    },
    async ping() {
      const res = await jsonFetch("/health", { method: "GET" });
      return res.ok;
    }
  };
}
function readStringArg(args, key) {
  const value = args[key];
  if (typeof value !== "string" || !value) {
    throw new Error(`tangle-id: missing required argument "${key}"`);
  }
  return value;
}

export {
  ResourceContention,
  CredentialsExpired,
  validateConnectorManifest,
  assertValidConnectorManifest,
  DEFAULT_TANGLE_PLATFORM_URL,
  TANGLE_API_KEY_PREFIX,
  TANGLE_SERVICE_TOKEN_PREFIX,
  TangleIdentityUnreachableError,
  tangleIdentity,
  createTangleIdentityClient
};
//# sourceMappingURL=chunk-Q5X3QNHR.js.map